前回は全くコーディングしませんでしたので,今回は少し遊んでみたいと思います。
F# Interactive
F# は対話モードで遊べる機能があります。今回はこれを利用します。
$ dotnet fsi
入力の終了はセミコロン2つ (;;
) で指定します。通常のコードでも ;
が登場するので,混ざるのを避けるためだと思われます。MySQL でストアドプロシージャを記述するときに一時的にデリミタを変更するのと同じ動機ですね。
最後に評価した結果は it
という変数にバインド(束縛)されます。
(これに限らず対話モードだと普通に再バインドできるみたいです)
対話モードを抜ける場合は #q
を指定します。
(Q# みたいで紛らわしいw)
参考:
https://docs.microsoft.com/ja-jp/dotnet/fsharp/tutorials/fsharp-interactive/
関数
let
で変数・関数を定義します。
> let f x y = x + y ;;
val f : x:int -> y:int -> int
> f 2 3 ;;
val it : int = 5
う~ん。シンプルに記述でき過ぎて,逆に戸惑うw
なんて遊んでいると,職場のK先生から「こんなのあるよ」と見せてもらったのがコチラ。
let f = (+)
「ん?何が起こるんですか?これ??」
教えて頂きました。x + y
と (+) x y
が等価ということらしいです。プラスを括弧で囲むと引数を二つとって合計を返す関数になり,それを f
にバインドしたってことみたいですね。
> let f = (+) ;;
val f : (int -> int -> int)
> f 2 3 ;;
val it : int = 5
(int -> int -> int)
となるのは,カリー化と部分適用の話のようです。
順番にやるとこうなります。
> f 2 ;;
val it : (int -> int) = <fun:Invoke@2684>
> it 3 ;;
val it : int = 5
まず f 2
を評価すると,引数を1つ受け取ると 2
を足して返す関数が返ってきます。さらに,これに 3
を渡すと合計した結果 5
が返ってくるというわけです。
括弧付きだとこうですね。
> f (2) (3) ;;
val it : int = 5
なるほど。
JavaScript だとこんな感じですね。
> const f = x => y => x + y;
> f (2) (3);
5
タプル
引数2つ取って反転させて返す関数のつもりだけど・・・
> let swap x y = y x ;;
val swap : x:'a -> y:('a -> 'b) -> 'b
> swap 2 3 ;; // error
あれ、、なんか y
が関数と推論されてるな。
> swap 2 (fun x -> x + 3) ;;
val it : int = 5
あぁ~複数の値を返したいときはタプルにするんですね。
> let swap x y = y, x ;;
val swap : x:'a -> y:'b -> 'b * 'a
タプルの中身を直接バインドできます。
さらに as
を使うとタプルと中身どっちもバインドできます。
> let a, b as t = swap "a" "b" ;;
val t : string * string = ("b", "a")
val b : string = "a"
val a : string = "b"
うん。ややこしいw
リスト
次はリストで遊んでみます。
(ちなみに配列は要素が可変,リストは不変な感じみたいです)
生成
> let a = [ 1; 2; 3 ] ;;
val a : int list = [1; 2; 3]
> let b = [ 4 .. 10 ] ;;
val b : int list = [4; 5; 6; 7; 8; 9; 10]
> let c = a @ b ;;
val c : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
> let d = 0 :: c ;;
val d : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
a. 要素を全て指定
b. 範囲を指定
c. リストの連結
d. 先頭に追加(HEAD と TAIL のパターンマッチ)
変換
> let sq arr = [ for i in arr -> i * i ] ;;
val sq : arr:seq<int> -> int list
> sq [ 1 .. 10 ] ;;
val it : int list = [1; 4; 9; 16; 25; 36; 49; 64; 81; 100]
Python のリスト内包表記みたいな構文で新しいリストがつくれます。
sq
はリストを受け取って,各要素を2乗したリストを返します。
お馴染みの map
や filter
もあります。
> List.map (fun x -> x * x) [1 .. 10] ;;
val it : int list = [1; 4; 9; 16; 25; 36; 49; 64; 81; 100]
> List.filter (fun x -> x % 2 = 0) [1 .. 10] ;;
val it : int list = [2; 4; 6; 8; 10]
興味深いのは List.map2
という関数。リストを2つ渡すと zip 的なことができます。
> List.map2 (fun x y -> x, y) ["a1"; "a2"; "a3"] ["b1"; "b2"; "b3"] ;;
val it : (string * string) list = [("a1", "b1"); ("a2", "b2"); ("a3", "b3")]
長さが違うリストを渡すとエラーになります。
集計
> List.sum [1 .. 10] ;;
val it : int = 55
> List.average [1. .. 10.] ;;
val it : float = 5.5
合計と平均。
List.average
は int
だとエラーになるので float
にしています。1.
は 1.0
と同義で float
になります。ちなみに,F# の float
は C# でいう double
らしいです。ややこしい。。
> List.sumBy (fun x -> x * x) [1 .. 10] ;;
val it : int = 385
> List.averageBy (fun x -> float x) [1 .. 10] ;;
val it : float = 5.5
List.averageBy
を使うと平均する前に各要素に適用させる関数を渡せます。これで float
にキャストすれば int
のリストでも OK ですね。
ソート
> [4, 3, 5, 2, 1] |> List.sort ;;
val it : (int * int * int * int * int) list = [(4, 3, 5, 2, 1)]
あれ、、ソートされないですね。
カンマで区切っちゃうと要素が1つのタプルのリストと認識されちゃうんですね。
セミコロンが正しい区切り文字でした。
> [4; 3; 5; 2; 1] |> List.sort ;;
val it : int list = [1; 2; 3; 4; 5]
というか,このセミコロンって改行区切りの意味なんですね。
わざわざ一行ずつ書くとセミコロンは要りません。
> [
- 2
- 3
- 5
- 2
- 1
- ]
- |> List.sort
- ;;
val it : int list = [1; 2; 2; 3; 5]
sortWith
を使うと比較関数を渡してソートできます。これで降順にしてみます。
> [4; 3; 5; 2; 1] |> List.sortWith (fun a b -> b - a) ;;
val it : int list = [5; 4; 3; 2; 1]
再帰
普通の再帰
> let rec sum arr =
- match arr with
- | [] -> 0
- | h::t -> h + sum t
- ;;
val sum : arr:int list -> int
> sum [1 .. 10] ;;
val it : int = 55
末尾再帰最適化
> let sum arr =
- let rec sumR acc arr =
- match arr with
- | [] -> acc
- | h::t -> sumR (acc + h) t
- sumR 0 arr
- ;;
val sum : arr:int list -> int
> sum [1 .. 10] ;;
val it : int = 55
良さげですね😉
再帰するには関数に rec
を付ける必要があるみたいです。
参考:
https://docs.microsoft.com/ja-jp/dotnet/fsharp/language-reference/lists
パターンマッチ
試しに Codewars の初級問題を解いてみます。
▽ Calculate BMI
https://www.codewars.com/kata/57a429e253ba3381850000fb
体重と高さを渡したら BMI に対応したメッセージを返す関数をつくる問題です。
先ほどの再帰でも使いましたが,パターンマッチを使います。when
で対象の変数に対する条件を追加することができます。手元で試したときに int
か float
かを指定しないとエラーになってしまったので,引数の型を明示的に指定しています。
> let bmi (w:float) (h:float) =
- match (w / (h * h)) with
- | x when x <= 18.5 -> "Underweight"
- | x when x <= 25.0 -> "Normal"
- | x when x <= 30.0 -> "Overweight"
- | _ -> "Obese"
- ;;
val bmi : w:float -> h:float -> string
> bmi 60.0 1.75 ;;
val it : string = "Normal"
つづく
ということで,おなかいっぱいになってきたので,今回はこのへんで✋