ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

2. 関数の基本 :: Swift プログラマのための Haskell 入門

さて、前回で Haskell の開発環境を構築できたので本編に入っていきたいと思います。

プログラミング言語の入門記事としては、基本データ型などから入るのがセオリーですが、そうした話題は大切ではあるものの同時に退屈でもあります。プログラマとしてある程度経験を積んでいれば、そうしたものはコードを読めば大体判断できますし、必要になってからあらためて理解すれば十分でしょう。

そのようなわけで、あえて関数から説明に入ってみたいと思います。

関数の基本

Swift で足し算をする関数のコードは次のように書けます。

func add(x: Int, y: Int) -> Int {
    return x + y
}

// 呼び出し
add(x: 1, y: 2) // => 3

一方、Haskell では次のように書きます。

add :: Int -> Int -> Int
add x y = x + y

-- 呼び出し
add 1 2 -- => 3

見た目がかなり異なりますが、比較すると次のことが読み取れるでしょう。

  • 引数と戻り値の型は -> で連結して記述する。
  • 引数ラベルは存在しない。
  • 呼び出すときは スペース で区切る。

少し詳しく見ていきましょう。

関数シグネチャの宣言

先ほどの Haskell のコードで1行目は関数シグネチャ、すなわち関数がどういう型を持つのかを定義しています。

add :: Int -> Int -> Int

これは「addIntInt を受け取り Int を返す関数である」と読むことができます。

引数と戻り値の区別がされていないことに違和感を持つかもしれませんが、これは Haskell の関数がデフォルトでカリー化されており「どこまでが引数であるか?」を考えることにあまり意味がないからです。その点については後述します。

なお、Swift では関数シグネチャの型を省略することができませんが、実は Haskell では省略可能となっており、関数の定義から型推論が可能であればこの行は無くてもコンパイルできます。

とはいえ、人間が読む際は型シグネチャがあったほうが分かりやすいので、一般的には記述するのが基本になっていますし、この連載記事でも記述するようにしていきます。

関数適用

ここまで「関数の呼び出し」と表現してきましたが、関数型言語では関数を適用するという表現がよく使用されます。

例えば f(x) というコードについて、関数 f を x に適用すると表現したりします。動作的にはどちらも変わらないのですが、Haskell の入門書などでも適用という言葉が使われていますし、ここからはこちらの言葉を使いたいと思います。

さて、関数適用は add 1 2 というとてもシンプルな記述で行われています。

純粋型関数型言語として関数が主役な Haskell ではスペースが関数適用(関数呼び出し)を表すようになっています。Swift のように引数ラベルといったものも使用できないため、慣れないうちは非常に読みづらいかもしれません。

ここで察しの良い方は、引数の間も スペース で区切られているのは構文としておかしいのではないか、と疑問を持たれるかもしれません。これについては本記事の後半で触れていきます。

where / let - in

Swift では大きめの関数を定義する際、計算の途中結果を一時的に格納するローカル変数などを用いたりします。

func calc(_ x: Int, y: Int) -> Int {
    let a = x * 100
    let b = y * 10
    return a + b
}

calc(1, 2) // => 120

Haskell では where を使って、一時的な変数や関数を定義できます。

calc :: Int -> Int -> Int
calc x y = a + b
    where
        a = x * 100
        b = y * 10

あるいは let-in 式を使った書き方も利用できます。

calc :: Int -> Int -> Int
calc x y = 
    let 
        a = x * 100
        b = y * 10
    in a + b

where vs let-in

where は必要な計算を後でで記述するスタイル、let-in 式は事前に記述するスタイル、とも表現できるかもしれません。

それぞれ声に出して読んでみると分かりやすいかもしれません。where の定義では・・・

calc x y = a + b
    where
        a = x * 100
        b = y * 10

calc x ya + b である。なお、ax * 100by * 10 である。」と読むことが出来ます。

一方、let-in では・・・

calc x y = 
    let 
        a = x * 100
        b = y * 10
    in a + b

calc x y は、ax * 100by * 10 であり、結果は a + b である。」と読むことが出来ます。

命令形言語に慣れていると let-in 式のほうが読みやすく感じるかもしれませんが、主要な計算式(ここでは a + b)が最初に現れるため、Haskell では where を使った記述のほうが読みやすいという人が多いようです。

ここまで書くと where のみを使えば十分に聞こえますが、where は関数のスコープに限定されるという制限があります。それに対して let-in 式は、ただの式なのでどこでも記述できます。

とはいえ、最初は where だけを覚えておけば十分かもしれません。

レイアウトルール

どちらもインデントを合わせることが重要であることを覚えておきましょう。例えば、以下のコードはコンパイルエラーとなります。

calc :: Int -> Int -> Int
calc x y = a + b
    where
        a = x * 100
      b = y * 10 -- compile error (parse error on input ‘b’)

Haskell ではレイアウトルールと呼ばれるものによってコードの書き方が規定されているため、インデントに誤りがあるとコンパイルエラーとなります(この点は同じくインデントベースである Python に似ているかもしれません)。

{}; を使った書き方もありますが、あまり一般的に使用されないためここでは割愛します。

ローカル関数

前述のコードではどちらも計算結果の変数のみを定義していましたが、ローカル関数を定義することも可能です。

where の例のみ記載します。

add :: Int -> Int -> Int
add x y = f x y
    where
        f a b = a * 100 + b * 10

必要であれば型注釈をつけることもできます。

add :: Int -> Int -> Int
add x y = f x y
    where
        f :: Int -> Int -> Int -- 型宣言を追加
        f a b = a * 100 + b * 10

型注釈

さて、ここまでで勘の良い方は気づかれたかもしれませんが、Swift で型注釈をする際に : 型 と記述するのに対して、Haskell では :: 型 と記述します。

// Swift での型注釈
let message: String = "Hello"

let add: (Int -> Int) -> Int = { $0 + $1 }
-- Haskell での型注釈
message :: String
message = "Hello"

add :: Int -> Int -> Int
add x y = x + y

: の数や、Swift では殆どの場所で書けるのに対して Haskell では記述する場所に制限があるなどの違いはありますが、どちらもコンパイラによる型推論のヒントまたは可読性のために記述する点は一緒です。

if 式

ここまで見てくると、次に if-else の書き方を知りたくなってくるのがプログラマというものでしょう。引数に渡された数値について、正の数だった場合に True、負の数だった場合に False を返すコードを見比べてみます。

Swift ではこうですね。

func isPositive(_ x: Int) -> Bool {
    if x >= 0 {
        return true
    } else {
        return false
    }
}

Haskell では次のようになります。

isPositive :: Int -> Bool
isPositive x = if x >= 0 
    then True 
    else False

一行にまとめて書いても大丈夫です。

isPositive x = if x >= 0 then True else False

いくつか違いが見えてくるでしょう。

  • Haskell における Bool は大文字始まりの TrueFalse である。
  • Haskell では if-then-else という構文で記述する。
  • Haskell では式となっており結果を返している。

Swift における if は文であるため結果を返すことはできません。一方、Haskell では if は式であるため値を返します。そのため Haskell では else を省略する、すなわち値を返さないコードは許可されていません。

勘の良い方はお気づきかと思いますが、Haskell での if-then-else は Swift での三項演算子に当たると考えられます。どちらも一行で記述して比較してみると分かりやすいでしょう。

// Swift
x >= 0 ? true : false
-- Haskell
if x >= 0 then True else False

ガード

条件が複数あって場合分けをするような場合、すなわち if-elseif に相当するコードはどのようになるのでしょうか。得点に応じて評価の文字列を返す関数を定義してみます。

Swift では次のようになるでしょう。

func judege(_ x: Int) -> String {
    if x == 100 {
        return "Perfect"
    } else if x > 90 {
        return "Great"
    } else if x > 50 {
        return "Good"
    } else if x > 30 {
        return "Bad"
    } else {
        return "Poor"
    }
}

どうやら著者は音ゲーマーであると推論できそうですが、それはさておき Haskell ではこのような場合にガードという記法が用意されています。

judge :: Int -> String
judge x
    | x == 100  = "Perfect"
    | x > 90    = "Great"
    | x > 50    = "Good"
    | x > 30    = "Bad" 
    | otherwise = "Poor"

どちらも上から順番に評価され、条件に合致したものが評価されます。otherwise は Swift での else に相当するものです。

なお、ここでは = の位置を揃えていますが、これはインデントではないため揃えなくてもコンパイルエラーになりません。しかし、Haskell ではこうした記号の位置を揃えることが多いため、この連載記事でも基本的に揃える形式で記載していきます。

カリー化・部分適用

単純な構文の比較もそろそろ飽きてきた頃でしょう。そろそろ関数型プログラミングらしいトピックをこのあたりで入れて読者のモチベーションを保つのに努めたいものです。

さて、カリー化という言葉を聞いたことがあるでしょうか?Swift が発表された当時から学んでいる方や、関数型に興味を持たれている方はご存知かもしれません。

カリー化とは複数の引数を受け取る関数を、1つずつ引数を受け取れる関数にすることです。といっても文章にしても何やら分からないと思うので Swift のコード例を見ていきたいと思います。

以下は何度も登場している add 関数ですが、この関数は引数を2つを同時に渡さないと機能しません。

func add(_ x: Int, _ y: Int) -> Int {
    return x + y
}

add(1) // compile error

それは当たり前だと感じるかもしれませんが、Swift ではクロージャを利用することで引数を順番に適用できるようになります。

func add(_ x: Int) -> ((Int) -> Int) {
    return { y in
        x + y
    }
}

let f = add(1) // 新たな関数が返る
f(2) // => 3

このようにクロージャを利用して関数の結果として新たな関数を返すようにすることで、引数を1つずつ適用できる関数を作れます。このように変更することをカリー化と呼び、新しいバージョンの関数 addカリー化された関数などと表現されます。

さて、このコードでは最初に add(1) を呼び出し、返却された関数を変数 f に代入しています。このように引数の一部を埋めることを部分適用と言ったりします。引数全体を適用していないから部分適用なのですね。

この例ではシンプルすぎて無駄にコード量を増やしてトリッキーなことをしているだけに感じるかもしれませんが、カリー化や部分適用は非常に有用なテクニックで、それを活用した Swift のライブラリなども存在します。Argo などが有名どころでしょうか?

github.com

さて、Haskell の場合はデフォルトで全ての関数がカリー化されています。add 関数を再掲します。

add :: Int -> Int -> Int
add x y = f x y

() で優先順位を明確にすると、以下のようになっています(Swift のコードと対比させると分かりやすいですね)。

add :: Int -> (Int -> Int)
add x y = f x y

部分適用について、Swift コードに対応させて書くと次のようになります。

calc :: Int
calc = f 2 -- => 3
    where
        f = add 1

ここまでくると、関数適用がスペースであるにも関わらず add 1 2 といったように引数もスペースで区切れている理由がわかるかと思います。

() で優先順位を明示すると ((add 1) 2) という計算が行われているのです。Swift では add(1)(2) という書き方に対応することになります。

さて、関数定義の説明のところで、引数と返り値の区別があって無いようなものだと記載しましたが、カリー化と部分適用に触れることで、その理由が少し分かってきたのではないでしょうか?

add 1Int -> Int な関数を返し、add 1 2Int を返しますし、単に add としたときは Int -> Int -> Int な関数(つまり add そのもの)を返す、関数であると考えることができます。すなわち、いくつの引数を部分適用するかによって返り値は変わってくるとも言えますし、引数と返り値の区別は無いものだと考えることも出来ます。

まとめ

といったところで今回のまとめです。

  • 関数シグネチャは add :: Int -> Int -> Int のように記述する。
  • 本体は add x y = x + y のように記述する。
  • 関数適用は add 1 2 のように記述する。
  • where でローカルな変数や関数を記述できる。
  • let-in 式でも同様のことができる。
  • :: 型 で型注釈を付けられる。
  • if-then-else は式で、Swift の三項演算子にあたる。
  • 条件が複数ある場合にはガード構文を利用できる。
  • カリー化は複数の引数を受け取る関数を、1つずつ引数を受け取れるようにすること。
  • カリー化された関数に一部の引数だけを与えることを部分適用という。
  • Haskell では全ての関数はカリー化されている。
  • そのため関数の引数と返り値の違いは曖昧になっている。

次回はデータ型の宣言やパターンマッチなどに触れていく予定です。

blog.penginmura.tech

あとがき

1,500円でプロギアの嵐が無限クレジットで遊べる Capcom Arcade Stadium は最高ですね。

Written by tobi462.