タイダログ

もっと怠けますか? (y/n)

F# のモジュールと変換関数の命名について

モジュールおよび変換関数の命名について考えたのでまとめます。きっかけは、Kit Eason 氏の Stylish F# 6: Crafting Elegant Functional Code for .NET 6 という書籍を読んだことと、デカルト座標とスクリーン座標を変換するための F# ライブラリ Fermata.CoordinateSystems を作ったことです。

github.com

www.nuget.org

執筆時点でのバージョンは 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 to fromMilesPointYards

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)>] 属性を付けてやり過ごしましょう。

stackoverflow.com

変換関数は "to型名" と "of型名" を両方定義する

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変換先 の関数だけでいいような気もしますが、両方定義することのメリットとして、処理の方向がわかりやすくなることを感じました。

たとえば、CartesianScreen に変換した後、その内容を標準出力に出すにはこうします。

// 点を描画する領域と原点を定義し、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 の方に向かって動きます。

「変換元.to変換先」のイメージ

流れの方向が |> の方向と合っているため、パイプの中で使うのに適しています。

「変換元.to変換先」はパイプと相性がいい

一方、変換先.of変換元 の関数は、データが右から左に流れるイメージです。「先←元」です。英語の "of" には「~から」という「起源」や「材料」の意味があります。"This table is made of wood." なんて英語の授業で習いませんでしたか? 私は教えましたよ(元英語科教員ですからね!)。

Screen.ofCartesian で「スクリーン座標、デカルト座標から」と読めます。"of" より "from" の方が「~から」のイメージが強いと思いますが、FSharp.Core に合わせて "of" を使っています。

「変換先.of変換元」のイメージ

この方向は let 束縛との相性がいいように思います。「左辺←右辺」の流れです。

「変換先.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# の書籍を読んで勉強して、ライブラリまで作ったので、その中で学んだことをまとめました。今後は、ただ動くものを作るのではなく、読みやすくメンテナンスしやすいコードを書きたいと思います。

参考

更新