前回につづきまして,fsi (F# Interactive) を使って遊んでみたいと思います。
関数合成
合成演算子 (>>
) は2個の関数を受け取って合成した関数を返します。
> let f1 x = x + 2
- let f2 x = x * 2
- let f3 = f1 >> f2
- ;;
val f1 : x:int -> int
val f2 : x:int -> int
val f3 : (int -> int)
> f3 5 ;;
val it : int = 14
f1 は 2 を足す。f2 は 2 を掛ける。f3 は f1 と f2 を合成。f3 5
を実行すると,まず f1 が呼ばれて,その結果で f2 が呼ばれて,その結果が返っていることが分かります。
((5 + 2) * 2) = 14
逆向き <<
もできます。
> (f1 << f2) 5 ;;
val it : int = 12
この場合は先に f2 が呼ばれて,次に f1 が呼ばれていますね。
((5 * 2) + 2) = 12
パイプライン処理
似た感じのものでパイプライン処理というのもあります。シェルのパイプのようなもので,前段の結果を後段の引数として渡してチェーン形式で書けます。Elixir にも同様の機能がありますね。なんと Ruby でも導入が検討されてるという話もあります。1
旧来の関数の呼び方だと括弧が入れ子になるので,呼び出し階層が深くなると読みづらくなります。パイプラインを使うと順番に並ぶので読みやすくなります。
> let add x y = x + y ;;
val add : x:int -> y:int -> int
> (add 5 (add 4 (add 3 (add 2 1)))) ;;
val it : int = 15
> 1 |> add 2 |> add 3 |> add 4 |> add 5 ;;
val it : int = 15
もう一つ,上記の例だと興味深いことが起こっています。パイプラインの前段から受け取る値は1つですが,add
は x と y を受け取ることになっています。もう一つの引数を部分適用することで引数が1つの関数に変化しているため,パイプラインでつなぐことができるようになっています。
パイプラインも逆向きにできますが,連続した場合は複数の引数を渡すという意味になるみたいです。というか,1コ目のが部分適用されて,それに対して2コ目のを適用させてるのかな。
> add <| 2 <| 3 ;;
val it : int = 5
合成演算子と組み合わせることで全体的に逆の流れをつくれます。
> add 5 << add 4 << add 3 << add 2 <| 1 ;;
val it : int = 15
でも,まぁ実際はこんなの混在させたら可読性が低くて仕方ないと思いますけどね。。
シーケンス
シーケンスは IEnumerable<T>
のエイリアスらしいです。ほほう。ということは,遅延評価的な感じなんですかね。(厳密には違うけど)
> seq { 1 .. 10 } ;;
val it : seq<int> = seq [1; 2; 3; 4; ...]
Seq モジュールは,シーケンスに関連するヘルパー。
fold
は JS でいうと Array.reduce のようなものですね。
> seq {1 .. 10} |> Seq.fold (fun acc x -> acc + x) 0 ;;
val it : int = 55
でも reduce
っていうのもありますね。こっちは初期値を受け取りません。第一要素がアキュムレーターの初期値になります。JS だと初期値を省略したときはこっちの挙動になりますね。
> seq {1 .. 10} |> Seq.reduce (fun acc x -> acc + x) ;;
val it : int = 55
シーケンス式
シーケンス式なるものがありますね。
というか,seq {1 .. 10}
もその1つです。
なんか色々できます。
例えば,複数のシーケンスをネストさせるような処理は以下のように書けます。
> seq {
- for a in 1..3 do
- for b in 1..3 do
- yield sprintf "%d*%d=%d" a b (a * b)
- } ;;
val it : seq<string> = seq ["1*1=1"; "1*2=2"; "1*3=3"; "2*1=2"; ...]
参考
https://docs.microsoft.com/ja-jp/dotnet/fsharp/language-reference/sequences
コンピュテーション式
コンピュテーション式は,マクロで DSL をライブラリ化するための仕組みです。構文の意味を特定のドメインに特化した形にカスタマイズできます。実は,シーケンス式もコンピュテーション式の一種です。F# コアには,他にも非同期ワークフローやクエリ式(DBを扱う)があります。また,ワークフロービルダークラスを作成して独自のワークフローを定義することもできます。
Haskell の do記法に似た感じのもので,主にモナドを扱うためのものですが,必ずしもモナド専用というわけではありません。
それでは簡単なの作ってみます。
> // 1.
- type ScriptBuilder() =
- member this.Bind(p, rest) =
- printfn "let! %A" p
- rest p
- member this.Delay(f) =
- fun () -> f()
- member this.Return(x) =
- x
- ;;
type ScriptBuilder =
class
new : unit -> ScriptBuilder
member Bind : p:'b * rest:('b -> 'c) -> 'c
member Delay : f:(unit -> 'a) -> (unit -> 'a)
member Return : x:'d -> 'd
end
> // 2.
- let script = new ScriptBuilder() ;;
val script : ScriptBuilder
> // 3.
- let exp = script {
- let x = 3
- let! y = 4
- return x + y
- }
- ;;
val exp : (unit -> int)
> // 4.
- exp() ;;
let! 4
val it : int = 7
1.まずは,Builder (ScriptBuilder) を定義します。
-
Bind
はlet!
キーワードのときの挙動をラップします。
ここではprintfn
を噛ませています。 -
Delay
は計算式を関数としてラップします。
ここでは結果を返す関数を定義しています。 -
Return
は return キーワードで呼び出されます。
ここではそのまま返しています。
2.続いて,Builder のインスタンスを script
にバインドします。
これで script { ... }
が使えるようになります。
3.スクリプト式を実行して exp にバインドします。
4.exp を実行します。
let! 4
が出力されて,7 が返ります。
もし,Delay
を入れてなければ,script { ... }
の時点で let! 4
が出力されることになります。
参考:
Computation Expressions(※公式の日本語版は訳がへん)
https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions
F# の基本を越えて - ワークフロー
https://www.infoq.com/jp/articles/pickering-fsharp-workflow/
つづく?
そろそろ fsi では厳しくなってきたなぁ(汗
それでは Happy Holidays 🎅 (2019/12/24)