0. まず知っておくべきこと
- F# は 静的型付け・型推論・非純粋・正格・(原則的に)Immutable
- さくっと試す
- ブラウザで試す: WebAssembly上。結構使いやすい。
- VisualStudioなら Alt+Enter で現在行を評価。範囲選択して Alt+Enter も使う
- 関数呼び出しに括弧は付けない
printfn "%d Hello %s" 1 "World"
-
printfn
: intは%d
, floatは%f
, stringは%s
, それ以外%A
を使う - 実は全部
%A
でもイケる(参考: フォーマット文字列)
- 言語リファレンス 特に 組み込み関数 / Core / System はすぐ見れるようにしておく
- インデントに意味があることがある(オフサイドルールという)
1. 文法速習
以下暫く 言語ツアー を超圧縮したもの:
1-1.データ型
bool: true
,false
に対し&&
,||
,not
が論理演算。
string: "
で囲む。@"a\tb"
のように@を付けるとエスケープを無効化。"""
で囲むとクォートのエスケープも無効化。"h"+"ello"
で結合"hello".[1..2]
で部分文字列。printf系 特にsprintf
を使うと文字列を構築しやすい。F#コアとしての文字列関数と .NET のSystem.Stringのインスタンスメソッド(Trim
等)を使い分ける必要があるので混乱しないように。
Tuple: (1,"two",3.0)
のように括弧でまとめればタプルになる。括弧は無くてもカンマさえあればよいのだけれど あった方が読みやすい: 1,"two",3.0,4L,5UL,6.0m
。
ListとArray: List は[1;2;3]
や[1..100]
で構築。簡単な処理なら内包的に: [ for i in 1..100 -> i + 1]
そうでないときは List.map
等を使う。[|
と|]
に置換するだけで同じことができ Arrayと呼ばれる。Arrayはランダムアクセスに強く、並列計算でも使うし、要素を追加することができる。
Seq: 遅延リスト。 seq { yield 1; yield 2 }
やseq { 1 .. 100 }
のように初期化したり seq { for s in s1 do if s.Contains("l") then yield s }
のように内包的に処理可。
Record: 名前付きタプルみたいなやつでメソッドもくっつけられる。(原則として)Immutableなので=
やマッチにおいて 参照でなく値で判定できるので便利。参考:クラスとの違い
Class: クラスやジェネリッククラスがある。member
キーワードを付与しなければ 変数を隠蔽できる。例えば member
つけずにmutable
つけると外部からアクセスできないが内部から変更できる状態を持たせることができる。interface ... with
でインターフェースの実装ができる。
DU:discrimated union: 事前に与えたどれか一つの型を取る(例:ジャンケン type Hand = | G | C | P
)。single-case DU (例: type Score = Score of int
)はドメインモデリングによく使う。DUは再帰的に定義もできる:
type BST<'T> = // 二分木のDU
| Empty
| Node of value:'T * left: BST<'T> * right: BST<'T>
1-2.関数合成・関数適用・ラムダ式
>>
は関数合成。|>
は 関数合成+適用。両者ともに関数の部分適用がなされると読むと理解しやすい。ラムダ式は fun x -> x + 1
。
let isOdd n = n % 2 = 1
let square n = pown n 2
let addOne n = n + 1
let f1 values =
values
|> List.filter isOdd
|> List.map (fun x -> x |> square |> addOne)
let f2 = List.filter isOdd >> List.map (square >> addOne)
// f1 [1..10] は [2; 10; 26; 50; 82]
// f2 [1..10] は [2; 10; 26; 50; 82]
1-3. パターンマッチ
基本match ... with
でパターンマッチをする。引数をそのままマッチにかける時には function
式で少しカッコよくかける。以下の「フィボナッチ数列のx番目を求める関数」の例では fib の宣言部にxが書かれていない:
let rec fib = function
| 1 -> 1
| 2 -> 1
| x -> fib (x - 1) + fib (x - 2)
少し応用的な例として「k回 List を巡回左シフトする」のはこんな感じで書ける。二つの引数をTupleに解釈してマッチ。::
を含むマッチ部では HeadとTailにdeconstructして順番をひっくり返して連結:
let rec rotate k xs =
match (k, xs) with
|_, [] -> []
|0, xs -> xs
|k, x::xs -> rotate (k-1) (xs @ [x])
その他にも、関数適用+結果でパターンマッチという Active Pattern という方法もある。
2. レシピ
身近にF#を書く人がいないのでAtCoderでの他の回答を見て、「こう使うのかー」と自分的に勉強になったテクニックをまとめる。
2-1. 行入力処理
「ストリームから1行読んで、スペースで分割して、それぞれintにキャスト」という処理パターン。特に多いのが ストリームが標準入力のケース:
let scanInt () = stdin.ReadLine() |> int
let scanInts () = stdin.ReadLine().Split() |> Array.map int
let [|a;b|] = scanInts ()
Splitの有無で scanInt
, scanInts
という二つを定義。使い方も勉強になって、deconstruct(これはClojure/JavaScriptでいうところの destructuring)が Array(やListでも)において可能とわかる。注:コンパイラが(要素数が2じゃないときに)変になるよとwarningを出してくるので、本来なら match を使ってそうでないときの挙動をハンドルしたほうがよい。
2-2. stringを列として扱う
「文字列に含まれるXの数を数える」ような場合を考える。いくつか方法があって
-
System.String
のインスタンスとしてToCharArray
メソッドを使う - コアの
String
の関数(collect
,map
等)を使う - 直接
Seq
のストリーム処理系関数に渡す
// System.String instance method (.NET)
"abXcXX".ToCharArray() |> Array.filter ((=) 'X') |> Array.length
// using Core.String
"abXcXX" |> String.collect (fun c -> if c = 'X' then "X" else "") |> String.length
// using pass it to Seq
"abXcXX" |> Seq.filter ((=) 'X') |> Seq.length
2-3. 実践的な再帰+マッチ
典型的なパターン:「処理対象であるタプル/レコードを Listに突っ込んで、要素数が0になる時を再帰の終了条件にして、再帰をする」というのはこんな感じでやる。要素数が1以上の場合に::
を使った句にマッチしてHeadとTailにdeconstructできるというのがポイント。末尾再帰への変更は忘れずに:
let rec mysum cnt xs =
match xs with
| [] -> cnt
| (n, _)::tl -> mysum (cnt + n) tl
printfn "%d" (mysum 0 [(1,"one");(2,"two");(3,"three")]) // 6
2-4. 二次元Listの転置
StackOverflowで見つけた。ちょっと読んだだけでは意味が分からないが、リンク先の説明が詳しくて理解できたような気がする:
// https://stackoverflow.com/questions/3016139/help-me-to-explain-the-f-matrix-transpose-function
let rec transpose = function
| (_::_)::_ as M -> List.map List.head M :: transpose (List.map List.tail M)
| _ -> []
3. 最初のうちにやるべきなサイト
- F# のツアー: MS公式の少し長めのチュートリアル。読むだけなら1時間。手を動かしても2時間あれば読める。よくできてる。
- FSharpKoans: エラーのでるテストケースを潰しながら進めるというタイプの ドリル。20セクションくらいあって、1つあたり 20分くらいあればできる。VSCode上でやる場合、.NET Core 2.x と Ionide (VSCodeでF#をするためのプラグイン) と C# のデバッグツール のインストールが必要。上に記載した F# のツアーの続きとしてやるのにちょうどいい程度の難度だと思う。
- F# for fun and profit: F#公式からもイチオシされている 超有能サイト。分量も多い。F#だけでなく、関数型な考え方も身につく(ドメインモデリングとかの設計がすごく勉強になった)。英語は平易なので読みやすい。