Skip to content

Instantly share code, notes, and snippets.

@znd-milktea
Last active August 29, 2015 13:58
Show Gist options
  • Save znd-milktea/10141941 to your computer and use it in GitHub Desktop.
Save znd-milktea/10141941 to your computer and use it in GitHub Desktop.

すごいHaskell読書会 in 大阪 2週目 #4 第5章「高階関数」

##この章の概要

この章では、「高階関数」を扱います。

高階関数とは、関数を引数/戻り値として扱う関数です。
但し、「高階関数」と特別な呼び方をするものの、関数をファーストクラスオブジェクトとして扱う言語からすれば、通常の関数と何ら変わりはありません。

高階関数のおかげで、カリー化や部分適用をHaskellで扱うことができます。

ラムダ式は、名前無しで関数を定義できます。高階関数を用いたプログラミングでは、ラムダ式を利用するとコードが見易くなる場合があります。

foldl/foldrは、リストに対する繰り返し処理を一般化した高階関数です。
実は、sum/map/filter等、これまでに紹介されたリストを扱う関数は、全てfoldl/foldrで定義できます。
foldl/foldrは、Haskellでのリスト処理を理解するのに重要な関数です。

関数合成は、2つの関数を受け取り、結果の関数を返す高階関数として定義されています。
関数合成によって、簡単な関数の組み合わせで、複雑な関数を定義することができます。


##5.1 カリー化関数 ###カリー化 関数maxの例は、2引数関数が、カリー化によって1引数関数の連鎖で表されることを示しています。
Haskellには、厳密に言えば複数引数の関数が存在しません。
それは、カリー化によって、複数引数の関数は全て1引数の関数の連鎖で表現できるからです。

f(x, y, x) --> F(x)(y)(z)

関数maxの呼び出し

max 4 5

(max 4) 5

の括弧を省略することで、あたかも複数引数の呼び出しのように見えているだけです。

ちなみに、カリー化の名前の由来はズバリ、皆さん大好き(?)なHaskellの名前の由来にもなった数学者、Haskell Curryです。

###部分適用 関数multThreeの例は、部分適用についての説明です。
カリー化された関数の適用の途中経過を、残りの引数を持つ複数引数の関数と解釈することも出来ます。

F(1)(y)(z) --> F'(y)(z) --> f'(y, z)

カリー化の仕組み上、Haskellで部分適用を行う場合は、左側の引数から順番に行うことになります。
「3番目の引数にのみ値を渡す」ということは直接出来ません(そういうラッパー関数を定義する)。

「カリー化」と「部分適用」は、良く混同されるので注意が必要です。

  • カリー化→複数引数の関数を、「1引数の関数の連鎖」に変換する操作
f(x, y, x) --> F(x)(y)(z)
  • 部分適用→複数引数の関数の一部の引数に値を渡し、残った引数のみの関数を得る操作
f(1, y, z) --> f'(y, z)

###ちょっと寄り道 Haskellの関数curryは、2要素のタプルを受け取る関数を、2引数関数(→1引数関数の連鎖)に変換します。

curry :: ((a, b) -> c) -> a -> b -> c

複数引数を「値の組み合わせ」と考えれば、値の組み合わせをタプル(構造体)で表現しているだけ、本質的には複数引数についてのカリー化と同じです。

###セクション 「セクション」では、2項演算子に代表される、中値関数について定義されている、特別な部分適用の文法である「セクション」について説明しています。

通常の部分適用は、適用される引数は左側から決める必要がありますが、セクションの場合は、左右どちらの引数でもOKです。

例外は減算演算子(-)です。同じ記号で単項演算子が定義されているため、(-4)は単項演算子を4に適用したとみなされます。
なお、(4-)はセクションとみなされます。
減算演算子の右側に部分適用した関数を定義する場合は、関数subtractを用います。 但し、

subtract x y = y - x

であることに注意してください。

###参考 カリー化談義 - あどけない話


##5.2 高階実演 ###型宣言の-> 型宣言の->は右結合です。なので、

関数を返す関数

a -> (b -> c)

には括弧は不要です。

a -> b -> c

でも同じ意味です。

関数を引数に取る関数

(a -> b) -> c

には括弧が必要です。

###高階関数と部分適用の組み合わせ 関数applyTwiceの例は、高階関数と部分適用の組み合わせを示しています。
部分適用によって引数の数を減らして、applyTwiceの引数の型に合うようにしています。

###再帰的な高階関数 関数zipWith'の例は、再帰的な高階関数を示しています。
引数として受け取った関数を、再帰を用いて、リストの各要素に適用しています。

###関数を戻り値とする高階関数 関数flip'の例は、関数を戻り値とする高階関数を示しています。 最初の

flip' :: (a -> b -> c) -> (b -> a -> c)

は、「2引数関数を受け取り、2引数関数を返す関数」という解釈です。

2番目の

flip' :: (a -> b -> c) -> b -> a -> c

は、「2引数関数と引数2つを受け取り、値を返す関数」という解釈です。
この場合、後ろの引数2つを与えない場合は部分適用となり、(b -> a -> c)型の関数が返されることになります。
これは、最初の解釈と同じになります。


##5.3 関数プログラマの道具箱 ###Preludeを活用しよう 関数mapは、リストの各要素に関数を適用した結果のリストを返します。
関数filterは、述語として与えられる関数をリストの要素に適用して、Trueを返す要素のみのリストを返します。

mapもfilterも、手続き型言語ではループで実装する処理を、高階関数として提供しています。
第4章で、ループに代えて、関数型言語では再帰で繰り返し処理を定義することを学びました。
しかし、基本的な繰り返し処理については、このように提供されている高階関数があるので、再帰を自前で定義する前に、提供されている道具箱(Prelude等)を探して、それらを利用できるかを検討してみてください。

###目的に合う一番力の弱い手段を使う Haskellの文法(再帰編) - あどけない話より

熟達した Haskeller は、以下のような力の階層を理解しています。

  • 再帰
  • foldr、foldl など
  • filter、take など

上が力が強く、下へ行くほど力が弱くなります。力が強いと何でもできてしまうので、コードの意図が不明瞭となり、また間違いが入り込みやすくなります。力が弱いとできることは限られるのでコードの意図は明確となり、間違いが入りにくくなります。

「目的に合う一番力の弱い手段を使う」のがよいプログラムを書くための大原則です。たとえば、草を刈るのにチェーンソーは使うべきではありません。もちろん草も切れますが、大切な植木も傷つけてしまうかもしれませんから。

###欲しい値の集合を得るコツ 「mapとfilterのさらなる例」では、これらの道具を利用してHaskellのコードを書く際のコツを示しています。
欲しい値の集合を得る場合の、手続き型言語とHaskellとのイメージの対比は下記の通りです。

  • 手続き型言語では、空集合に対して条件に合う値を追加していく(リストに追加等)
  • Haskellの場合は、候補となる値の集合全体(場合によっては無限)を定義し、そこから条件に合う値をフィルタリングしていく

##5.4 ラムダ式 ###引数にラムダ式 関数numLongChainsの例は、高階関数への引数にラムダ式を用いる方法を示しています。

  • 通常の関数定義→名前がある
  • ラムダ式による関数定義→名前が無い

高階関数の引数や戻り値としての関数は、呼び出し先や戻り先の変数で参照できるので、定義する際に関数名を付けても、用いられない場合があります。そのよう場合にラムダ式を用いると、余分な関数名の宣言を省くことができます。

###戻り値にラムダ式 関数flip'の例は、高階関数の戻り値にラムダ式を用いる方法を示しています。
flip'の意味合いとして「引数を入れ替えた"関数を返す"」なので、関数の定義でも、戻り値が関数であることを表したほうが自然です。
そこで、ラムダ式を用いることで、戻り値が関数であることを明示しつつ、関数名の宣言を省略することができます。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment