Haskell を触ってみる

Haskell・純粋関数型言語 への興味

オブジェクト指向でなぜつくるのかの最終書は関数型言語への言及でした。そこで出されてたクイズ。

純粋関数型言語Haskell がサポートしてないな仕組みはどれでしょうか?
(複数回答可)
① 戻り値が void 型の関数定義
② return 文による関数からの復帰
③ ローカル変数の書き換え
④ for 文によるループ処理

正解は「全てサポートされてない」で、え?まじかと思って関数型言語に興味がわきました。

こんなときにどうするの?

これを見てぱっと浮かんだ疑問です。

① 戻り値が void 型の関数定義
-> まぁこれはなくても書けるか。

② return 文による関数からの復帰
-> ガード節的なのはどうするの?

③ ローカル変数の書き換え
-> これもなくても大丈夫そう?だけど言語レベルでサポートしてるって斬新

④ for 文によるループ処理
-> map 使うのか?どうやって定義するの?

ガード節的なのはどうするの?

例えば割り算をする関数があったときに、分母になる値が 0 の時だけ割り算させないように 0 を返したい場合。

Javascript の場合

ガード節を使って以下のように書きます。

function warisan(x, y) {
  if(y === 0) return 0

  return x / y
}
 
console.log(warisan(9, 3)) // => 3
console.log(warisan(9, 0)) // => 0

haskell の場合

パターンマッチで事前に弾く。

-- ハイフン 2 つでコメントアウト
-- 型定義
warisan :: Int -> Int -> Int -- 引数と戻り値の型

-- 関数定義
-- 分母が 0 の場合、0 を返す
warisan x 0 = 0
-- それ以外の場合は割り算をして値を返す
warisan x y = (x `div` y)  -- / は int の割り算では使えないので div を使う

普通に if も書ける。

-- 関数定義
warisan x y =
  if y == 0
    then 0
    else (x `div` y)

ガードという書き方(| を使って分割する)を使うともう少し見やすく場合分けが書ける。

-- 関数定義
warisan x y | y == 0 = 0
            | otherwise = (x `div` y)

参考)ここで書かれてる haskell で書かれた fizzBuzz 関数はガードを使って綺麗に書かれてる。
関数型言語Haskellでfizzbuzzプログラムを作成する

ループ処理どうやってやるの?

再帰関数とパターンマッチを使う。

再帰関数の勉強

階乗を求める関数 factorial を作る。
factorial(3) => 3 + 2 + 1 = 6

再帰関数を使わない場合

function factorial(n) {
  let result = 0;
  for(let i = n; i > 0; i--){
    result = result + i
  }
  return result;
}

これを再帰関数にすると?

function factorial(n) {
  if(n === 1) return 1
  return n + factorial(n - 1)
}

これを Haskell にすると?

factorial :: Int -> Int
factorial 1 = 1
factorial n = n + factorial(n - 1)

each を作る

配列の値を 1 つずつ取り出して表示させる関数を作る。
each([1, 3, 10]) =>
1
3
10

再帰関数を使わない場合

function each(array) {
  for (let element of array) {
    console.log(element);
}

これを再帰関数にすると?

function each(array) {
  element = array[0]
  new_array = array.slice(1) // 配列の最初の値を抜いた配列を取得

  if(array.length === 1) return console.log(element)
  
  console.log(element)
  each(new_array)
  return
}

これを Haskell にすると?
表示する関数(print)を foreach に渡してあげる必要があるんだね。

-- 返り値の型が分からないから型定義はしない
-- 型推論) > :t foreach :: Monad m => (t -> m a) -> [t] -> m a
-- Monad が分からないね…
foreach f [element] = f element
foreach f (element:new) = do f element
                             foreach f new

main = foreach print [1,10,3]

参考)Haskell > 繰り返し処理(foreach相当)を行う

map 関数を作る

haskell には map 関数は存在しますが、map 関数の仕組みを再帰関数で書いてみたかったので Javascript で map 関数を作ってみる。

array = [1, 10, 3]
console.log(map(array, increment)) // => [2, 11, 4]

再帰関数を使わない場合

function increment(n) {
  return n + 1
}

function map(array, transform) {
  mapped = []
  for(let element of array) {
    mapped.push(transform(element))
  }
  return mapped
}

再帰関数(自分で考えて書いた => 複雑になった…)

function map2(array, transform) {
  return recursion0(array, array.length, [], transform)
}

function recursion0(array, length, mapped, transform) {
  if(length <= 0) return

  mapped.push(recursion0(array, length - 1, mapped, transform))
  return mapped
}

再帰関数(haskell の map の実装を見て書き直した)

function map3(array, transform) {
  if(array.length <= 0) return []

  x = array.slice(0, 1)
  xs = array.slice(1)
  return [transform(x[0])].concat(map2(xs, transform))
}

参考)お気楽 Haskell プログラミング入門

filter, reduce 関数を作る

map と同様、haskell には filter 関数、foldr, foldl 関数(reduce と同じ)がありますが仕組みを理解したかったので再帰関数で書いてみる。

github.com

感想

  • もっちーとの勉強会のおかげで勉強サボらずにできた。約束の力👊
  • Haskell のコードを Javascript で書き換えたりしたので、Haskell のコードをだいぶ読めるようになった👏
  • 階乗の再帰関数なら javascripthaskell もコードの見やすさは変わらないけど、map など複雑さを増してくると haskell の宣言的な書き方の方がスッキリしてる。
  • パターンマッチが強力で引数の配列の最初の値だけ束縛することができるのもコード量を減らしてるポイントだと思った。
  • haskell のサンプルを見たときに()がなく、スペースで判断なことが多かったので読み辛く感じた。
  • Ruby関数型言語っぽく書ける部分が多いので「if, while を使ったループ」と「ローカル変数の書き換え」はあんまりやってないことに気付いた。
  • map 関数、filtter 関数、reduce 関数の実装楽しかった。自分で作ったのだと無駄が多かったので、haskell の実装(正解)を見て感動した。
  • 高級関数をきちんと学べてよかった。
  • 今後:モナドを頑張って勉強したい…!文脈を持つ計算とは??

参考