Hatena::ブログ(Diary)

みずぴー日記

2011-12-16(金)

ちょっと便利な関数の紹介

| ちょっと便利な関数の紹介 - みずぴー日記 を含むブックマーク

(* この記事は、F# Advent Calendar 2011の16日目の記事です。 *)

こんにちは、mzpです。 クリスマスたのしみですね。

さて、関数プログラマは自分だけの関数を持っていることが多いものです。 ライブラリ化にするほどの規模でないので、コピペによって各プロジェクトを移動しながら、洗練されたり新しいオペレータが追加されれたりします。

というわけで今日はOCamlハカーのosiireさんのオペレータを紹介していきます。

ネタ元

F#標準

まずはF#だと不要な関数です。

let id x = x
let (@@) f x = f x
let (+>) f g = g f
let (+$) f g x = f (g x)

それぞれid, (<|), (|>),(<<)に対応してます。「え、こんなのもデフォルトで定義されてないんですか(笑)」と言うと効率的にOCamlerをdisれるのでおすすめです。

Haskellからの輸入

Haskellで定義されている関数を輸入した関数です。

let curry f x y = f (x, y)
let uncurry f (x, y) = f x y
let flip f x y = f y x

順番を入れ替えたりできるので、引数の形を変更するだけの無名関数を書くのを避けれます。

> List.map (fun (x,y) -> x + y) [(1,2); (3,4)] 
val it : int list = [3; 7]

> List.map (uncurry (+)) [(1,2); (3,4)]
val it : int list = [3; 7]

無限ループ

無限ループするための関数です。

let rec forever f x =
  let v = f x in
  forever f v

同じ処理を繰り返すプログラムの本体部分で使ったりします。

let _ =
  forever read_eval_print ()

例外とOption型の変換

let maybe f x = try Some (f x) with e -> None
let may x f = match x with None -> None | Some v -> Some (f v)
let may_default d f = function Some v -> f v | None -> d
let default d = function Some v -> v | None -> d

option型を返さずに例外を投げてくる関数を扱うための関数です。

maybe List.hd xs
|> may do_something_for_head
|> default 0

ただ複数引数とる関数の場合は、ちょっと不恰好になります。

(* List.tryFind相当 *)
maybe (List.find x) xs

デバッグ

let tee f x = ignore (f x); x

Unixのteeコマンドのように処理を挟みこんだりするときに便利です。

do_A ()
|> do_B
|> tee printfn
|> do_C

みたいにすれば、パイプライン中の値を覗けます。

最後に

人のコード読むのはたのしいですね。

mclh46mclh46 2011/12/16 07:35 この記事を読んで、自分のやり方を書いてみました。
http://d.hatena.ne.jp/mclh46/20111216/1323986621
F#の標準のライブラリは、1つの関数、クラスの利用範囲が広く、また弱点のある道具は不採用になっていると思われます。
maybeやteeの考え方はとても参考になりました。

vebbytanakavebbytanaka 2018/06/21 16:05 になっていると思われます。
maybeやteeの考え方はとても参考になりました

pelangi4dpelangi4d 2018/06/21 16:06 つの関数、クラスの利用範囲が広く、また弱点のある道具

2011-02-05(土)

F#プログラマのためのMaybeモナド入門

| F#プログラマのためのMaybeモナド入門 - みずぴー日記 を含むブックマーク

f:id:mzp:20110203222658j:image

http://twitpic.com/3w34bo

はじめに

モナドといえばHaskellHaskellといえばモナドが有名ですが、モナドは特定の言語とは無関係の仕組みですので、F#でも使えます。ただ単に使えるだけでなくコンピュテーション式というモナドをより便利に使うための文法まで用意されています。

この記事では、option型を便利に扱うためのMaybeモナドを例にモナドのすごさを紹介していきます。

Maybeモナドのすごいとこ

option型っていいですよね。ぬるぽので落ちる心配もないですし。Maybeモナドはそんなoption型を便利に扱うためのモナドです。

option型を扱うのにMaybeモナドと呼ばれてるのは、Haskell由来だからで特に重要な理由はありません。

DBからの値取得とかHttpリクエストからのパラメータ取得などの外部とやりとりする関数は必ず成功するとか限らないのでoption型を返す関数として実装するのが安全です。でもoption型を返す関数を繋げていくとネストが深く、読みづらくなってしまいます。

例えば、 dbというMapに格納されている"x"と"y"を加算する関数は次のようになります。

(* 値が格納されたMapを作る *)
let db = Map.ofList [("x", 1); ("y", 2); ("z", 3)]

(* dbに格納されている"x"と"y"を加算する関数 *)
let x_and_y =
    match Map.tryFind "x" db with
    | Some x ->
        match Map.tryFind "y" db with
        | Some y ->
            (* xとyが取得できたので加算して返す *)
            Some (x + y)
        | None ->
            (* yの取得に失敗したのでNoneを返す *)
            None
    | None ->
        (* xの取得に失敗したのでNoneを返す *)
        None

(*
  実行結果
  val it : int option = Some 3
 *)

ネストが深いくて読みづらいですね。この例は2つだからまだいいですが、3つや4つの値を扱うことを考えるとげんなりしてきます。

これをMaybeモナドを使って書き直すと次のようになります。

let x_and_y' =    maybe {
        let! x = Map.tryFind "x" db (* 失敗したら全体がNoneになる *)
        let! y = Map.tryFind "y" db (* 失敗したら全体がNoneになる *)
        return x + y (* xとyを加算して返す *)
    }

(*
  実行結果
  val it : int option = Some 3
 *)

ネストが浅くなってるしコードの量も減って読みやすくなっています。 しかもMap.tryFindのどちらかがNoneを返したらx_and_y'もNoneになるので、安全性も失われていません。

さあ、みんなもMaybeモナドを使いましょう!

Maybeモナドの裏側

f:id:mzp:20110205120429j:image

http://www.flickr.com/photos/audra_b/3831200/

さて、なぜこんなことができるのかを見ていきましょう。

コンピュテーション式による変換

maybe { ... }はコンピュテーション式と呼ばれる構文で、コンパイル時に次のような変換が行なわれます。(一部抜粋)

{¦ let! pattern = expr in cexpr ¦}builder.Bind(expr, (fun pattern -> {¦ cexpr ¦}))
{¦ return expr ¦}builder.Return(expr)

おおまかに言うとlet!がbuilder.Bindに、returnがbuilder.Returnに変換されます。

この変換規則に従って、先程の式は次のように変換されます。

(*
先程の式:
 let x_and_y' =     maybe {
         let! x = Map.tryFind "x" db
         let! y = Map.tryFind "y" db
         return x + y
     }
*)

(* コンピュテーション式を展開した式 *)
let x_and_y' =
    (* let! x = Map.tryFind "x" db *)
    maybe.Bind(Map.tryFind "x" db, fun x ->
      (* let! y = Map.tryFind "y" db *)
      maybe.Bind(Map.tryFind "y" db, fun y ->
         (* return x + y *)
         maybe.Return(x+y)))
Maybeモナドの定義

ここでのmaybeはBindとReturnを実装したMaybeBuilderクラスのインスタンスです。MaybeBuilderクラスは次のように定義されています。

type MaybeBuilder() =
    member this.Bind(x, f) =
        match x with
        | Some y -> f y
        | None -> None

    member this.Return(x) =
        Some x

let maybe = new MaybeBuilder()

コンピュテーション式を展開した式のBindとReturnも展開すると、次のようになります。

(* コンピュテーション式を展開した式
 let x_and_y' =
     maybe.Bind(Map.tryFind "x" db, fun x ->
       maybe.Bind(Map.tryFind "y" db, fun y ->
          maybe.Return(x+y)))
 *)
let x_and_y' =
    (* maybe.Bind(Map.tryFind "x" db, fun x -> ...) *)
    match Map.tryFind "x" db with
    | Some x ->
        (* maybe.Bind(Map.tryFind "y" db, fun y -> ...) *)
        match Map.tryFind "y" db with
        | Some y ->
            (* maybe.Return(x+y) *)
            Some (x + y)
        | None ->
            None
    | None ->
        None

結局、モナドを使わない場合と同じ式になりました。

まとめ:なにがすごいか

Maybeモナドを使うことで、optionを扱うコードを短く書くことができました。

今回はoptionを扱うコードを短く書くMaybeモナドを紹介しただけですが、他にも状態付きの計算を行なうためのStateモナドや非決定計算を行なうためのListモナドなどがあります。詳しくはAll About Monadsなどを参照してください。

このようにDSLをF#の枠組みの中で作ることができる上に、優秀なHaskellianの方々が○○モナドを既に大量に生み出しているので、それを活用することができるのがモナドのすごいところです。

さあ、今日からモナドを使ってプログラミングしましょう!

補足:モナドって何よ?

普通にモナドを使ってプログラミングする上では問題になりませんが、モナドとなるにはモナド則を呼ばれる規則を厳密に満たす必要があります。

けっして、コンピュテーション式で使えるのがモナドだったり、ReturnとBindを実装したのがモナドではありません。

詳しくはモナドを作りたくなったら、調べるぐらいでいいと思います。

参考文献

2010-10-04(月)

F#はじめました

| F#はじめました - みずぴー日記 を含むブックマーク

30分プログラム、その805。

インストール

使い方

$ mono $FSHARP_HOME/bin/fsc.exe
$ cp $FSHARP_HOME/bin/FSharp.Core.* .
$ mono fact.exe
3628800

ソースコード

let rec fact n =
    if n = 0 then
        1
    else
        n * (fact (n - 1))

printf "%d\n" (fact 10);;

参考

2009-01-05(月)

F#を試してみた

| 14:17 | F#を試してみた - みずぴー日記 を含むブックマーク

30分プログラム、その503。OCamlに似てるけど、互換性はそれほどないと評判のF#を触ってみた。

ちょっとしか触っていないけど、内包表記があったり、小粋な演算子(|>とか<|とか)が定義されていたりと、いい言語な気がする。今、学校でC#を使っているけれど、これをF#に置き換えれたら、かなり幸せになれる気がする。

ただ、Windowsでしか動かないから、使うのが結構面倒くさい。一応、MacPortsにも入っているから、あとで試そうと思う。

使い方

VisualStudioに読み込んで、Ctrl-F5で以下のような結果が得られる。

fact(10) = 3628800
fib(5)   = 120

ソースコード

#light

let rec fib n =
    if n = 0 then
        1
    else
        n * fib(n - 1)

let rec fact = function
   0 -> 1
 | n -> n * fact (n-1)

let _ =
    printfn "fact(10) = %d" <| fact 10;
    printfn "fib(5)   = %d" <| fib 5

参考

F#とOCamlの違い

| F#とOCamlの違い - みずぴー日記 を含むブックマーク

F# at Microsoft Research - Microsoft ResearchについてきたTutorial.fsを読みながら、F#とOCamlの違いついてメモしてみます。

拡張子が.fs

OCamlは.mlだけど、F#は.fs。

コメントが//

OCamlのコメントは(* ... *)だけど、F#は//。

範囲

let oneToTen = [1..10]

内包表記

/// The squares of the first 10 integers
let squaresOfOneToTen = [ for x in 0..10 -> x*x ]

ディクショナリ

let lookupTable = dict [ (1, "One"); (2, "Two") ]

let oneString = lookupTable.[1]

パイプライン

2008-11-12の+>と同じですね。

let squares3 = [1; 2; 3; 4] |> List.map (fun x -> x*x)

@@と同じの<|もある。

fact <| 1 + 2

型やフィールドが大文字で始まる

OCamlでこれって許されていたかな?どっちにしろ、違和感がある。

type Card = { Name  : string;
              Phone : string;
              Ok    : bool }

クラスのインタフェース

お、だんだんずれてきたぞ。

// Types: classes
// ---------------------------------------------------------------

/// A 2-dimensional vector
type Vector2D(dx:float, dy:float) =
    // The pre-computed length of the vector
    let length = sqrt(dx*dx + dy*dy)
    /// The displacement along the X-axis
    member v.DX = dx
    /// The displacement along the Y-axis
    member v.DY = dy
    /// The length of the vector
    member v.Length = length
    // Re-scale the vector by a constant
    member v.Scale(k) = Vector2D(k*dx, k*dy)


// Types: interfaces
// ---------------------------------------------------------------

type IPeekPoke =
    abstract Peek: unit -> int
    abstract Poke: int -> unit

print

// Print a result using %A for generic printing
printfn "listC = %A" listC

yoshihiro503yoshihiro503 2009/01/06 10:24 OCamlとF#の違いが一目でわかっていいですね。F#使うときに参考にさせていただきます。
ただ、 |> と <| は定義すればすむような。
# let (<|) f x = f x
# let (|>) x f = f x

mzpmzp 2009/01/06 11:01 ありがとうございます。たしかに|>や<|は自分で簡単に定義できますけど、それが標準で定義されているのがステキだと思います。
標準で定義されていれば、サンプルコードとかでも心置きなく使えますし。

いげ太いげ太 2009/01/06 14:02 こんにちは。

> それが標準で定義されているのがステキだと思います
非常に共感します。言語がそれを標準で提供しているか否かの差はデカイですね。共通見解や慣例を待たずして、パブリックな演算子としてそれを使えるわけですから。
僕は、|> や <|、また >> や << などが標準で定義されていることは非常に意義のあることだと思っていて、このことは、F# という言語がどういう方向性で作られているのかを象徴しているような気さえします。

ちなみに、|> と <| は F# ライブラリ上では以下のように定義されています。

let inline (|>) x f = f x
let inline (<|) f x = f x

inline がミソですねー。

okagawaokagawa 2009/01/06 19:53 コメントですが、F#でも (* と *)はコメント扱いされますよ。
また、(*IF-FSHARP...ENDIF-FSHARP*)(または(*F#...F#*))で囲まれた範囲はF#ではコメント扱いされませんが、OCamlでは当然コメントです。
一方、(*IF-OCAML*)...(*ENDIF-OCAML*)で囲まれた範囲は当然OCamlではコメント扱いされませんが、F#ではコメント扱いです。

okagawaokagawa 2009/01/06 19:57 おすすめかどうかはわかりませんが、INRIAのCamlチーム(?
gallium.inria.frなので勝手にそう思っている)にいる
日本人の方の書いたOCamlとF#の比較記事が下記のアドレスに
あります。

http://gallium.inria.fr/~nakata/FSharp.html

OCamlとF#のオブジェクトシステムの違いを論じているような気がしますが、読んでいません。。

mzpmzp 2009/01/06 23:17 有用なコメントありがとうございます。

> いげ太さん。
いげ太さんてhttp://igeta.cocolog-nifty.com/の方ですよね。F#の記事、楽しく読ましてもらっています。

> okagawaさん。
まさか条件付コメントまであるとは。しかも、実に巧妙ですねぇ。

yoshihiro503yoshihiro503 2009/01/07 10:01 ごめんなさい。osiire先生に指摘されて気づいたのですが、OCamlで定義できると言ったのは間違いでした。
OCamlでは < や | で始まる中置演算子は左結合になるため、|> はO.K.ですが、<| 演算子をつなげたときにカッコが必要になってしまいます。
OCamlではこのように記号によって有線順位や結合の向きが決まってしまいます。

いげ太いげ太 2009/01/09 11:08 > mzp さん
はい。いげ太@igeta.cocolog-nifty.com です。こちらこそ、いつも興味深く拝見させてもらってます。

> @@と同じの<|もある
@@ と <| は同じではないです。同じなのは、OCaml と F# の演算子における優先順位と結合規則の決定方法です。ですから、yoshihiro503 さんが言われている、OCaml 上で <| を @@ と同じく振舞わせるための懸念事項は、そのまま F# にも当てはまります。これはたとえば、以下のようなコードで試してみるとわかりよいかと思います。

let inline (@@) f x = f x

[1..5] |> List.map (fun x -> 2*x + 1) |> List.filter ((<) 5) // [7; 9; 11]
List.filter ((<) 5) @@ List.map (fun x -> 2*x + 1) @@ [1..5] // [7; 9; 11]
List.filter ((<) 5) <| List.map (fun x -> 2*x + 1) <| [1..5] // error!
List.filter ((<) 5) <| (List.map (fun x -> 2*x + 1) <| [1..5]) // [7; 9; 11]

このことは F# コミュニティでも問題視されているようで、「<| の動きってどうよ?」という話題が HubFS にもあがっています。またここで Don Syme は、この件に関して、future release での design change を示唆しています。

『<| has wrong associativity and precedence』
http://cs.hubfs.net/forums/permalink/5326/5326/ShowThread.aspx

どういう風に変えてくるかは見ものだなー(OCaml 互換的な意味で)なんて個人的には見守っているところです。