読者です 読者をやめる 読者になる 読者になる
ようこそ。睡眠不足なプログラマのチラ裏です。

はじめの一歩。まずはパイプライン演算子と合成演算子から。

プログラミング 関数型プログラミング F# F#上達法 息抜き

今月末の7月28日、29日と、Code2012という合宿イベントに参加する予定です。F#や関数型について語り合える人がいない場合は割とボッチになりそうな気もしていますが...、それならそれで適当に楽しんでこようと思っています。というわけで、いつものように「月末になったらブログ書こう...」と後回しにしていると、なにも書かずじまいで終わっちゃいそうな気がするので、心に余裕のあるうちになんか書いておきます。

わたしはこれでF#覚えました。これが上達の鍵です。えっと、割とマジです。



F#でZ会三年生中学受験コース5月のてんさく問題

さて、話はちょっと変わって、「Z会三年生中学受験コース5月のてんさく問題」というのをF#で書いてみます。



元ネタ
Z会三年生中学受験コース5月のてんさく問題を Scala で解いてみた - terazzoの日記
http://d.hatena.ne.jp/terazzo/20120708/1341775360

4けたの数について、それぞれの位の数字を大きいじゅんにならべた数から小さいじゅんにならべた数をひくという計算を行います。
1974 について、この計算を 100 回行った答えを書きなさい。

Z会三年生中学受験コース5月のてんさく問題を Python で解いてみた - cooldaemonの備忘録

とりあえず書いた。

  let tensaku = 
    let subAscFromDesc x = 
      let s = (string x).ToCharArray() |> Seq.map string
      let toInt x = String.concat "" x |> int
      let flip f x y =  f y x
      toInt(List.sortWith (flip compare) (Seq.toList s)) - toInt(Seq.sort s)
    [1..100] |> Seq.fold (fun x _ -> subAscFromDesc x) 1974

  printfn "%d" tensaku


この手のコードを書いていつも思うのが、誰もが必要としそうな関数があらかじめ標準的に用意されているか否かで、書き易さが俄然違ってくるということ。F#では標準の範囲内で文字列操作が扱いやすいとは決して言えない感がある。あと、compare関数がデフォであるのはうれしいんだけど、ならflip関数もデフォで欲しいような気もする。



ちょっと書き換える。sortWithを使わずに昇順で並べたものをreverseするようにしただけ。

  let tensaku = 
    let subAscFromDesc x = 
      let s = (string x).ToCharArray() |> Seq.map string
      let calc x = String.concat "" >> int |> fun f -> f (List.rev (Seq.toList x) |> List.toSeq) - f x
      Seq.sort s |> calc
    [1..100] |> Seq.fold (fun x _ -> subAscFromDesc x) 1974

  printfn "%d" tensaku

Arrowの(&&&)演算子は関数を並列にする


元ネタでterazzoさんが、ScalazのArrow(関数のArrow)を使って書いていたので、同じくArrow使って書いてみたい。いつもならFSharpxをよっこらせと引き出してくるところであるが、残念ながらFSharpxにArrowはない。そういえばあそこにありましたねということで、某Arrowの実装をちょっと利用して書いてみる。

  let tensaku = 
    let sorted x = (string x).ToCharArray() |> Seq.map string |> Seq.sort  
    let toInt x = String.concat "" x |> int
    let rev = Seq.fold (fun acc e -> Seq.append (Seq.singleton(e)) acc) Seq.empty 
    let sub (x,y) = x - y 

    let subAscFromDesc = 
      string >>> sorted >>> ((rev >>> toInt) &&& toInt) >>> sub
    [1..100] |> Seq.fold (fun x _ -> subAscFromDesc x) 1974

  printfn "%d" tensaku

これがArrowのちからか。もし出来合いのsorted、toInt、rev、subの関数があったならば、3行で書けちゃうとか。欲張ればワンライナーでもイケちゃいますね、みたいな。「でもそれ読みやすいか?」って言われると、大半の人は横に首を振ると思う。しかしながら、Arrowというのは別に読みにくいコードを書くためのものではない。なにか計算の本質的な部分を表しているような雰囲気がある。で、この例で特にミソとなるは、(&&&)演算子の部分で、昇順ソートと降順ソートの結果をタプルにまとめている部分。つまり、2つの関数を並列に繋いでいるところがミソ。ってまぁArrowとかよくわかっていませんが。



ちなみにHaskellのArrowの定義はこちら。

class Arrow a where
arr :: (b -> c) -> a b c
pure :: (b -> c) -> a b c
(>>>) :: a b c -> a c d -> a b d
first :: a b c -> a (b, d) (c, d)
second :: a b c -> a (d, b) (d, c)
(***) :: a b c -> a b' c' -> a (b, b') (c, c')
(&&&) :: a b c -> a b c' -> a b (c, c')


もちょっと手を加えてみる。ループを内側によっこしただけ。

  let tensaku = 
    let sorted x = (string x).ToCharArray() |> Seq.map string |> Seq.sort  
    let toInt x = String.concat "" x |> int
    let rev = Seq.fold (fun acc e -> Seq.append (Seq.singleton(e)) acc) Seq.empty 
    let sub (x,y) = x - y 
    let loop st so f = Seq.fold (fun x _ -> f x) st so

    let subAscFromDescLoop st so = 
      string >>> sorted >>> ((rev >>> toInt) &&& toInt) >>> sub |> loop st so
    [1..100] |> subAscFromDescLoop 1974

  printfn "%d" tensaku


で、Arrowを用いて書いてみたわけですが、ここで言うArrowは関数のArrowなので、別に某Arrowの実装を使う必要は全くなくて、ちょっとした工夫をして以下のようにするだけで同じことができる。

  let tensaku = 
    let sorted x = (string x).ToCharArray() |> Seq.map string |> Seq.sort  
    let toInt x = String.concat "" x |> int
    let rev = Seq.fold (fun acc e -> Seq.append (Seq.singleton(e)) acc) Seq.empty 
    let sub (x,y) = x - y 
    let loop st so f = Seq.fold (fun x _ -> f x) st so
    let (&&&) f g = (fun x -> x, x) >> (fun f (a,b) -> f a,b ) f >> (fun f (a,b) -> a,f b) g

    let subAscFromDescLoop st so = 
      string >> sorted >> ((rev >> toInt) &&& toInt) >> sub |> loop st so
    
    [1..100] |> subAscFromDescLoop 1974

  printfn "%d" tensaku


このように関数のArrowにおいては、(>>>)演算子は、ただの関数の合成を意味しているので(>>)合成演算子にそのまま置き換えられるし、(&&&)演算子も上記のように割かし簡単に定義することができる。関数の合成という基本知識の範囲内で同じことができる。うんArrowが計算の本質的な部分を表しているような雰囲気があったのはこのためだね。



F#の上達法



パイプライン演算子についてはここであらためて説明する必要もない気もしますが、簡単に。(|>)パイプライン演算子という中置演算子を用いることで、関数と引数の順序を逆にすることができます。これによって、関数適用の流れを手続き型的に表現することができるというものです。F#ではこの「パイプライン演算子を使う」ことが基本でありながら、同時に最上級のプラクティスであると言っても過言ではなく。これを好んでよく使うことはおのずとF#の上達に繋がります。もう一つ、この記事でもたくさん使用した(>>)合成演算子で関数を合成する練習を行うのも良い。これは関数型プログラミングの基本的な考え方のひとつを身に着けるのに役立つ。他にも、mapしろだfoldしろだと上達のための"はじめの一歩"はいろいろとある。いきなりモナドとかゆー壮大な抽象概念に飛び込まなくたって関数型の魅力とパワーはそこかしこにあるし、まずは小さなことからコツコツと。中には飛び級できちゃう人もいるけどね。


関数型言語を用いた関数型プログラミングには、「今のはメラゾーマではない、メラだ 」がそこかしこに溢れているね!!!ダイの大冒険とかテラ夏カシスだね!!!



ところで、小学生的にはこのZ会の問題をどうやって解くのかな。と思ったのですが、実の所これ5回操作をすると6174(7641-1467=6174)に結果が収束するんですよね。ということで、今回定義した関数はメモ化しておいた方がよかったのか?どうでもいいね。