Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@pocketberserker
Last active August 29, 2015 13:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pocketberserker/8972681 to your computer and use it in GitHub Desktop.
Save pocketberserker/8972681 to your computer and use it in GitHub Desktop.

Functional忍者 5!

更新日

2014/03/14

書いた人

@pocketberserker

ひとまず試したい方へ

Try F# というオンラインサービスで、簡単な F# コードを記述、実行することができます。

開発環境の準備

開発環境構築について、簡単に説明します。

Windows で Visual Studio を使いたい場合

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
  | [] -> ... 

List

(TODO: List の説明を追加する)

Map

(TODO: Map の説明を追加する)

型略称

型略称を使うことで、コードを読みやすくすることが可能になります。

type type-abbreviation = type-name

Option へ

前回の忍者資料 に存在するコードを、片っ端から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 には filtergetOrElseorElse が存在しないので、自作する必要があります。

後半戦

まずは 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

それでは、 generateSimplegenerateComplex を 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

解答その1

パターンマッチで実装した関数を示します。

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

解答その2

ちょっとリファクタリングしてみました。

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

解答その3

コンピュテーション式を使って書き直してみたバージョンです。

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
  }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment