- 更新日
2014/03/14
- 書いた人
Try F# というオンラインサービスで、簡単な F# コードを記述、実行することができます。
開発環境構築について、簡単に説明します。
Visual Studio 2010 (Professional 以上)、Visual Studio (2012 Professional 以上)、Visual Studio 2013 (Professional 以上)の環境を既にインストールされている方は、既に F# 開発環境がインストールされています。次の章へお進みください。
Visual Studio Express 2013 上で F# を利用するためには、まず Visual Studio Express 2013 for Web をダウンロード、及びインストールしてください。
インストールが終了し、Visual Studio を起動したら、"ツール" -> "拡張機能と更新プログラム" から "Viasual F# Tools" を検索してダウンロード、インストールを行ってください。
Visual Studio を起動したら、 "Ctrl + Alt + F" ショートカットコマンドで F# Interactive を立ち上げてください。IDE 下部に F# Interactive が表示され、簡易な F# コードを実行する準備が整ったことでしょう。コードを保存しておきたい場合は、プロジェクトを作成するなり、 .fsx ファイルを作成して F# スクリプトとしてファイルを保存してください。
残念ながら、筆者は Windows + Visual Studio 以外の環境を所持していません。左記以外の環境については、後述する F# Software Founfation の "Getting F#" を参考にしてください。
なお、これ以降 Windows 以外の環境は基本的に考慮せず説明します。
各環境における、コードの実行方法は下記の通りです。
- F# Interactive: 実行したいコードの最後に";;"(セミコロン2つ)を記述する
- Try F#: "Run" ボタンを押す
- F# プロジェクト: プロジェクトをデバッグなしで実行、もしくはデバッグ実行してください
本節以降では、基本的に F# Interactive もしくは Try F# を利用していることを前提に説明を行います。
まずは、次のコードを入力してみてください。
1 + 2 // equal 3
スラッシュ2つ記述すると、それ以降の文字列はコメントとして解釈されます。もう一つ試してみましょう。
2 * 3.0 // コンパイルエラー!
F# は強い静的型付き言語なので、上記コードはコンパイルエラーになります。仮にこのコードを実行したい場合、どちらかの値を明示的に型変換する必要があります。今回は、整数型の結果を得ることにしましょう。
2 * (int 3.0) // equal 6
今度は実行できました。
計算が行えるようになったので、結果をどこかに保存したいですね。そこで、変数を使いましょう。
let x = 10 + 1
let
キーワードを用いるで、 10 + 1 の結果を x
に束縛しました。変数に値が束縛されると、その変数は値を変更できません。変数の値を変更する方法はありますが、今は必要ないので、ここでは触れないことにします。
まずは通常の関数から。
let f a b = a + b
f 1 2;; // equal 3
再帰関数を書きたい場合は rec
キーワードを用います。
(TODO: ラムダ式の説明を追加する)
let rec factorial n =
if n = 0 then 1
else n * factorial (n - 1)
factorial 5;; // equal 120
代表例として、2つの要素からなるタプルを定義してみましょう。
let x : int * string = (1, "one")
型はアスタリスクを使って表現します。
レコードは、名前付きのデータの集合体です。 レコードの単純な定義は、
type typename = {
label1 : type1
label2 : type2
...
}
となります。この定義方法のレコードでは、メンバーはimmutableです。
例として、名前と年齢を持つ Person 型を定義してみましょう。
type Person = {
Name : string
Age : int
}
データを生成してみましょう。
let hoge =
{
Name = "ほげ・ふーばー"
Age = 18
}
レコードの値を取得するには .
を使います。
hoge.Name // "ほげ・ふーばー"
既存のデータをコピーし、一部のデータを変更して新しいデータを生成することもできます。
{ hoge with Age = hoge.Age + 1 }
そのデータが複数の名前付きケースのいずれかを表したいとき、判別共用体を使います。
(TODO: 説明を追加する)
fizzbuzzしてみましょう。
let fizzbuzz n =
match (n % 3, n % 5) with
| (0, 0) -> "FizzBuzz"
| (0, _) -> "Fizz"
| (_, 0) -> "Buzz"
| _ -> string n
レコードのパターンマッチも可能です。
match x with
| { Name = "ほげ・ふーばー"; Age = age } -> ... // ほげ・ふーばーという名前を持つデータにマッチする
(TODO: 判別共用体のパターンマッチを追加する)
type Shape =
| Circle of float
| Rectangle of float * float
let hoge shape =
match shape with
| Circle r -> ...
| Rectangle (w, l) -> ...
関数の最後の引数をそのままパターンマッチしたい場合、 function
キーワードが利用できます。
let hogeMacth = function
| [] -> ...
(TODO: List の説明を追加する)
(TODO: Map の説明を追加する)
型略称を使うことで、コードを読みやすくすることが可能になります。
type type-abbreviation = type-name
前回の忍者資料 に存在するコードを、片っ端からF#化していきましょう。
まずは検索から。
let dict = Map.empty |> Map.add "one" 1 |> Map.add "two" 2
dict |> Map.tryFind "one" // Some(1): int option
dict |> Map.tryFind "three" // None: int option
次に中身を変えてみましょう。
let stringSucc strNum =
let num = dict |> Map.tryFind strNum
num |> Option.map (fun x -> x + 1)
stringSucc("one") // Some(2)
stringSucc("unknown")
Scala の Option.flatMap
は F# での Option.bind
です。
let stringAdd x y =
let optX = dict |> Map.tryFind x
let optY = dict |> Map.tryFind y
optX |> Option.bind (fun a ->
optY |> Option.map (fun b ->
a + b
)
)
stringAdd "one" "two" // Some(3)
stringAdd "one" "unknown" // None
F# の Option には filter
、 getOrElse
、 orElse
が存在しないので、自作する必要があります。
まずは Emotion を判別共用体で定義します。
type Emotion =
| Love
| Hate
次に、Conversation をレコードで定義します。
type User = string
type Conversation = {
Affinity: Map<User * User, Emotion>
Message: Map<User * Emotion, string>
}
必要なものが揃ったので、ここでは一気に実装します。
let template u1 u2 msg = sprintf "%s「%s?」\n%s「%s!」" u2 msg u1 msg
let inverse = function
| Love -> Hate
| Hate -> Love
let generateSimple (u1: User) (u2: User) conversation : string option =
if conversation.Affinity |> Map.containsKey (u1, u2) then
let emotion = conversation.Affinity.[(u1, u2)]
if conversation.Message |> Map.containsKey (u1, emotion) then
Some (template u1 u2 (conversation.Message.[(u1, emotion)]))
else None
else None
let generateComplex (u1: User) (u2: User) conversation : string option =
let { Affinity = affinity; Message = message } = conversation
if affinity |> Map.containsKey (u1, u2) && affinity |> Map.containsKey (u2, u1) then
let emotion1 = affinity.[(u1, u2)]
let emotion2 = affinity.[(u2, u1)]
if emotion1 = emotion2 then
if message |> Map.containsKey (u1, inverse(emotion1)) then
Some (template u1 u2 (message.[(u1, inverse(emotion1))]))
elif message |> Map.containsKey (u1, emotion1) then
Some (template u1 u2 (message.[(u1, emotion1)]))
else None
else None
else None
let affinity =
Map.ofList
[
(("fuga", "pocketberserker"), Love)
(("pocketberserker", "fuga"), Love)
(("fuga", "hoge"), Hate)
]
let message =
Map.ofList
[
(("pocketberserker", Love), "F#")
(("fuga", Love), "Smalltalk")
(("fuga", Hate), "静的型付き")
]
let conv = { Affinity = affinity; Message = message }
// シンプルなgenerate
conv |> generateSimple "pocketberserker" "fuga" |> printfn "%A\n" // Some "fuga「F#?」\npocketberserker「F#!」"
conv |> generateSimple "fuga" "pocketberserker" |> printfn "%A\n" // Some "pocketberserker「Smalltalk?」\nhoge「Smalltalk!」")
conv |> generateSimple "fuga" "hoge" |> printfn "%A\n" // Some "fuga「静的型付き?」\nhoge「静的型付き!」")
conv |> generateSimple "hoge" "fuga" |> printfn "%A\n" // None
conv |> generateSimple "pocketberserker" "hoge" |> printfn "%A\n" // None
// 複雑なgenerate
conv |> generateComplex "pocketberserker" "fuga" |> printfn "%A\n" // Some "fuga「F#?」\npocketberserker「F#!」"
conv |> generateComplex "fuga" "pocketberserker" |> printfn "%A\n" // Some "pocketberserker「「静的型付き?」\nhoge「静的型付き!」")
conv |> generateComplex "fuga" "hoge" |> printfn "%A\n" // None
conv |> generateComplex "hoge" "fuga" |> printfn "%A\n" // None
conv |> generateComplex "pocketberserker" "hoge" |> printfn "%A\n" // None
それでは、 generateSimple
と generateComplex
を Option を使った形式に修正していきましょう。
- いきなり Map 用関数で書き換えるのがつらいと感じたら、一度ラムダ式とパターンマッチで実装し、その後同じような処理を行う関数を Map モジュールから探してみてください。
Scala の for内包表記 のような形式で書きたい、という場合には、 F# ではコンピュテーション式を使います。ここでは、 Option 用のコンピュテーション式を定義します。
…とはいえ、ここでコンピュテーション式を解説するにはリソースが足りません。そのため、今回は定義したコンピュテーション式をどう使うかのみ説明します。
今回必要なコンピュテーション式は下記ように定義できます。
type OptionBuilder () =
member this.Zero() = None
member this.Return(x) = Some x
member this.Bind(x, f) = Option.bind f x
let option = OptionBuilder ()
コンピュテーション式内でもっと多くの式を利用できるようにすることも可能ですが、今回はこれで十分です。
Return メソッドを定義することで return
キーワードが、 Bind メソッドを定義することで let!
キーワードが使用可能になります。
今回のコンピュテーション式では、 return
に値を渡すことで値を Some で包み Option 型 にすることができます。また、 let!
で渡された Option 型のデータから値を取り出します。let!
に None を渡した場合は、 以降の処理結果がすべて None となります。
では、前述の stringAdd
関数を、コンピュテーション式を利用した実装に修正してみましょう。
let stringAdd x y =
option {
let! a = dict |> Map.tryFind x // 内部で変換が行われ、取り出した値が変数のように利用できる
let! b = dict |> Map.tryFind y
return a + b // 両方とも Some であれば計算結果が Some に包まれる
}
stringAdd "one" "two" // Some(3)
stringAdd "one" "unknown" // None
パターンマッチで実装した関数を示します。
let generateSimple (u1: User) (u2: User) conversation : string option =
match conversation.Affinity |> Map.tryFind (u1, u2) with
| Some emotion ->
match conversation.Message |> Map.tryFind (u1, emotion) with
| Some message -> Some u1 u2 message
| None -> None
| None
let generateComplex (u1: User) (u2: User) conversation : string option =
let { Affinity = affinity; Message = message } = conversation
match affinity |> Map.tryFind (u1, u2), affinity |> Map.tryFind (u2, u1) with
| Some emotion1, Some emotion2 when emotion1 = emotion2 ->
match message |> Map.tryFind (u1, inverse(emotion1)) with
| Some message -> template u1 u2 message
| None ->
match message |> Map.tryFind (u1, emotion1) with
| Some message -> template u1 u2 message
| None -> None
| _ -> None
ちょっとリファクタリングしてみました。
let generateSimple (u1: User) (u2: User) conversation : string option =
conversation.Affinity
|> Map.tryFind (u1, u2)
|> Option.bind (fun emotion ->
conversation.Message
|> Map.tryFind (u1, emotion)
|> Option.map (template u1 u2))
let orElse defaultValue = function
| Some v -> Some v
| None -> defaultValue
let generateComplex (u1: User) (u2: User) conversation : string option =
let { Affinity = affinity; Message = message } = conversation
match affinity |> Map.tryFind (u1, u2), affinity |> Map.tryFind (u2, u1) with
| Some emotion1, Some emotion2 when emotion1 = emotion2 ->
message
|> Map.tryFind (u1, inverse(emotion1))
|> orElse (message |> Map.tryFind (u1, emotion1))
|> Option.map (template u1 u2)
| _ -> None
コンピュテーション式を使って書き直してみたバージョンです。
type OptionBuilder () =
member this.Zero() = None
member this.Return(x) = Some x
member this.Bind(x, f) = Option.bind f x
let option = OptionBuilder()
let generateSimple (u1: User) (u2: User) conversation : string option =
option {
let! emotion = conversation.Affinity |> Map.tryFind (u1, u2)
let! message = conversation.Message |> Map.tryFind (u1, emotion)
return template u1 u2 message
}
let generateComplex (u1: User) (u2: User) conversation : string option =
let { Affinity = affinity; Message = message } = conversation
option {
let! emotion1 = affinity |> Map.tryFind (u1, u2)
let! emotion2 = affinity |> Map.tryFind (u2, u1)
if emotion1 = emotion2 then
let! message = message |> Map.tryFind (u1, inverse(emotion1)) |> orElse (message |> Map.tryFind (u1, emotion1))
return template u1 u2 message
}