モジュールおよび変換関数の命名について考えたのでまとめます。きっかけは、Kit Eason 氏の Stylish F# 6: Crafting Elegant Functional Code for .NET 6 という書籍を読んだことと、デカルト座標とスクリーン座標を変換するための F# ライブラリ Fermata.CoordinateSystems を作ったことです。
執筆時点でのバージョンは 0.1.0 です。
型とモジュールの名前を同じにする
Stylish F# 6 に以下のように書いてあります。
The first thing that might jump out at you is the naming of the
create
function. "Create" is a rather a vague word.We could perhaps rename
crate
tofromMilesPointYards
How about moving the function into a module with the same name as the type and renaming it
fromMilesPointYards
(Listing 2-9)?
Listing 2-9
open System type MilesYards = MilesYards of wholeMiles : int * yards : int module MilesYards = let fromMilesPointYards (milesPointYards : float) : MilesYards = // ...
Kit Eason 著 Stylish F# 6: Crafting Elegant Functional Code for .NET 6 p.19 より
Listing 2-9 の内容は、float
を受け取って MilesYards
という判別共用体を返す関数だそうです。イギリスの鉄道での距離の表し方を再現したコードだとか。
ここで重要なのは、MilesYards
判別共用体を扱うための関数を、同じく MilesYards
という名前のモジュール内で定義するということです。fromMilesPointYards
関数のフルネームは MilesYards.fromMilesPointYards
関数ということになります。こうすることで何のための関数なのかがわかりやすくなります。「MilesYards
を、MilesPointYards
から作るのね」ということです。
FSharp.Core でも List
を扱うための関数は List
モジュールに入っています。データ構造とそれに対する処理が同じ名前にまとまっていて分かりやすいと思います。この整理整頓の仕方は OOP のクラスに通ずるものがあるように感じます。
ちなみに F# 4.0 までは、型名とモジュール名を同じにするとコンパイラエラーが発生します。「F# 7.0 な私には関係ないね」と思っていたら、ブラウザ上でコードを実行できる素敵なサイト https://paiza.io が F# 4.0 を使用しているので関係ありました。モジュールに [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
属性を付けてやり過ごしましょう。
変換関数は "to型名" と "of型名" を両方定義する
FSharp.Core の List
モジュールには、List
から Array
に変換するための List.toArray
関数と、その逆をする List.ofArray
関数が備わっています。一方 Array
モジュールには Array
から List
に変換する Array.toList
関数とその逆の Array.ofList
関数があります。
私が作成した、デカルト座標とスクリーン座標の変換ライブラリ Fermata.CoordinateSystems でも、デカルト座標 (Cartesian
) とスクリーン座標 (Screen
) 間の変換関数として
Cartesian.toScreen
Cartesian.ofScreen
Screen.toCartesian
Screen.ofCartesian
の4つを定義しました。
変換元.to変換先
の関数だけでいいような気もしますが、両方定義することのメリットとして、処理の方向がわかりやすくなることを感じました。
たとえば、Cartesian
を Screen
に変換した後、その内容を標準出力に出すにはこうします。
// 点を描画する領域と原点を定義し、Cartesian を生成する let rect = { Rectangle.Width = 100.; Height = 100. } let origin = { Origin.X = 50.; Y = 50. } let cartesian = Cartesian.create rect origin 20. -10. // Screen 用の原点を定義する let screenOrigin = { Origin.X = 0.; Y = 0. } // Cartesian と、Screen 用の原点から、Screen に変換して出力する cartesian |> Cartesian.toScreen screenOrigin |> printfn "%A"
変換元.to変換先
の関数は、データが左から右に流れるイメージがあるかと思います。「元→先」です。英語の "to" には「~へ」という「方向」や「到達」の意味があります。Cartesian
から Screen
の方に向かって動きます。
流れの方向が |>
の方向と合っているため、パイプの中で使うのに適しています。
一方、変換先.of変換元
の関数は、データが右から左に流れるイメージです。「先←元」です。英語の "of" には「~から」という「起源」や「材料」の意味があります。"This table is made of wood." なんて英語の授業で習いませんでしたか? 私は教えましたよ(元英語科教員ですからね!)。
Screen.ofCartesian
で「スクリーン座標、デカルト座標から」と読めます。"of" より "from" の方が「~から」のイメージが強いと思いますが、FSharp.Core に合わせて "of" を使っています。
この方向は let 束縛との相性がいいように思います。「左辺←右辺」の流れです。
// 点を描画する領域と原点を定義し、Cartesian を生成する let rect = { Rectangle.Width = 100.; Height = 100. } let origin = { Origin.X = 50.; Y = 50. } let cartesian = Cartesian.create rect origin 20. -10. // Screen 用の原点を定義する let screenOrigin = { Origin.X = 0.; Y = 0. } // Cartesian と、Screen 用の原点から、Screen に変換して束縛する let screen = Screen.ofCartesian screenOrigin cartesian
以下は流れの方向がよく分からなくなった例です。
パイプ |>
は右向き、Screen.ofCartesian
関数は左向き、束縛も左向き。カオス。
結び
F# の書籍を読んで勉強して、ライブラリまで作ったので、その中で学んだことをまとめました。今後は、ただ動くものを作るのではなく、読みやすくメンテナンスしやすいコードを書きたいと思います。
参考
- Kit Eason 著 Stylish F# 6: Crafting Elegant Functional Code for .NET 6, Apress, 2022, ISBN 978-1-4842-7204-6
- https://stackoverflow.com/questions/44491043/does-f-guideline-recommend-to-declare-module-type-having-the-same-name:tilte
- CompilationRepresentationFlags (FSharp.Core)
更新
- 「変換関数は "to型名" と "of型名" を両方定義する」にイメージ画像を追加 (2023/03/01)