Haskell > 本 > 入門Haskellプログラミング > LESSON 3

2019年9月16日 公開
2020年5月14日 更新

入門Haskellプログラミング

#html{{

table border="0" cellpadding="5"><tr><td valign="top"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4798158666/vertex9-22/" target="_blank"><img src="https://images-fe.ssl-images-amazon.com/images/I/51o1oUqjsvL._SL160_.jpg" border="0"></a></td>

td> </td>

td valign="top"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4798158666/vertex9-22/" target="_blank">入門Haskellプログラミング</a><br>Will Kurt<br>翔泳社<br>2019-07-31<br>¥ 4,104</td></tr></table>
}}

LESSON 3

3.1 ラムダ関数

#html{{

div class="panel panel-danger">

 <div class="panel-heading">キーワード</div>
 <div class="panel-body">

}}

#html{{

 </div>

/div>
}}

書式

\x -> x

本書、p.26の図3-1には間違いがある。
「関数の引数(複数の場合もある)」から伸びている線は、右側のxに向かっているが、正しくは左側のxに向かっていなければならない。

ラムダ関数の書き方の例

Prelude> (\x -> x * 2) 4
8

型を確認してみます。

Prelude> :t (\x -> x)
(\x -> x) :: p -> p
Prelude> :t (\x -> x * 2)
(\x -> x * 2) :: Num a => a -> a

3.2 where句

#html{{

div class="panel panel-danger">

 <div class="panel-heading">キーワード</div>
 <div class="panel-body">

}}

#html{{

 </div>

/div>
}}

(参考)

補助関数とは、関数定義の内部でのみ使用する部分的な関数のことです。
補助関数を作成することで、複雑で分かりにくいプログラムの構造を簡潔にして可読性を向上することができます。
また、処理毎に分割することはプログラムを再利用しやすく、保守性のあるものにします。
なお、補助関数を内包する親となる関数は「最上位関数」と呼ばれます。
Haskellにおける補助関数の定義方法は「let」と「where」の2通りありますが、whereを使うほうが一般的といえます。
どちらを使用しても問題ありませんが、混在していると可読性が悪くなるため、統一して利用することが推奨されています。

3.3 let式

(p.29)

Haskellには、where句の代わりに使用できるlet式と呼ばれるものがあります。

where句とlet式の違いは?

素朴な疑問です。

基本
let 変数/関数 in 式という書式で、ローカルな変数や関数を定義できる。
letは式なので結果を返す。

whereとの違い
whereと似ているが以下の点が異なる。

  • どこでも書ける。
  • whereではガードをまたぐことが出来るが、letで定義したものはinの中でしか参照できない。

好みの問題もあるが、基本的にはwhereを使って、必要な場合はletを使うのが良さそう。

「ガード」による違いがあるみたいですね。
基本はwhereを使っておくことにしておきたいと思います。

補足

プログラミング学習では専門用語が次から次へと出てくるので、後で混乱しないように意味を押さえて覚えておきたい。

ラムダ関数

ラムダ関数は、別名で「無名関数」とも言ったりする。(言語によって違うのだろうか?)

ラムダ(λ)という記号の由来

些細なことだけど、素朴な疑問。
なぜ無名関数はラムダ関数という名称が与えられているのか?
そもそも「ラムダ関数」とか「ラムダ計算」の「ラムダ」ってどこから出てきたのか?

^(キャレット)記号

「^」この記号は何ていう名前なんでしょうか?

「キャレット」という記号は、正確には下側に位置する山形の記号だった。
上側に位置する山形の記号は、「サーカムフレックス」とか「ハット記号」という名前の別の記号だった。

^ ←サーカムフレックス(上側に寄っている山形の記号)
‸ ⁁ ←キャレット(下側に寄っている山形の記号)

両方とも単体で見た場合には形がそっくりだから、サーカムフレックスをキャレット(カレット)と呼ぶ場合もある。
ややこしいですね!

で、ラムダに話を戻すと、最初キャレット(本当はサーカムフレックス)を使って「^x」とか書いていたのを「λx」と書くようになった。
それが「ラムダ」という記号が登場するきっかけであり、ラムダ計算とかラムダ関数とか呼ばれる由来になったわけですね。

特に深い意味はなくて、気まぐれで使っただけみたいなので、ここは「なぜラムダなんだろう?」と悩むところではないのでしょう。
ただの定義というか、「へー、そういうもんなの?」と軽く受け流して、次行ってみよう!

クイックチェック

クイックチェック 3-1

クイックチェック 3-2

次の関数を書き換えて、whereの代わりにラムダ関数を使用してみましょう。

#code(haskell){{
doubleDouble x = dubs*2

 where dubs = x*2

}}

#code(haskell){{
doubleDouble x = (\dubs -> dubs*2) (x*2)
}}


この問題の解き方が分からなかった。

自分が考えた方法

  1. where句で定義されている名前が付けられた関数「dubs = x*2」を無名関数に変える。
    1. where dubs = x*2
    2. where dubs = (\x -> x * 2)
  2. (\x -> x * 2)を1行目のdubsのところに持って行く。
    1. doubleDouble x = (\x -> x * 2) * 2

ところが、

#code(haskell){{
doubleDouble x = (\x -> x * 2) * 2
}}
だとエラーメッセージが出て動かない。

<interactive>:7:1: error:
   ? Non type-variable argument in the constraint: Num (a -> a)
     (Use FlexibleContexts to permit this)
   ? When checking the inferred type
       doubleDouble :: forall a p. (Num a, Num (a -> a)) => p -> a -> a

確かに「* 2」は引数としては不適切であるようなことは、何となく分かる。
剥き出しの「* 2」では数値でもないし、関数でもないと思うので、型が決まらない値になってしまうのではないだろうか?

そこで剥き出しの「* 2」を「(* 2)」という具合にカッコでくくってみた。

Prelude> :t (* 2)
(* 2) :: Num a => a -> a

つまり、(* 2)は何かを2倍にする関数として扱われるはずだ。

#code(haskell){{
doubleDouble x = (\x -> x * 2) (* 2)
}}
これもエラーメッセージが出て動かない。

<interactive>:10:1: error:
   ? Non type-variable argument in the constraint: Num (a -> a)
     (Use FlexibleContexts to permit this)
   ? When checking the inferred type
       doubleDouble :: forall a p. (Num a, Num (a -> a)) => p -> a -> a

doubleDouble x = ...(関数の本体)
とイコールの右辺で引数xが使われてない形になっているためだろう。
そこまでは何となく推察してみたけど、「(x*2)」という具合で引数xを入れる意味、思考の過程が分からないのであった。

#code(haskell){{
doubleDouble x = (\x -> x * 2) (* 2) x
}}

これもエラーメッセージが出て動かない。

<interactive>:11:1: error:
   ? Non type-variable argument in the constraint: Num (a -> a)
     (Use FlexibleContexts to permit this)
   ? When checking the inferred type
       doubleDouble :: forall a. (Num a, Num (a -> a)) => a -> a

「(* 2) x」の部分はある数値xを2倍する関数として、構文的には記述できるはずだ。(中置演算子「*」のカリー化)

Prelude> dubs x = (* 2) x
Prelude> dubs 2
4
Prelude> :t dubs
dubs :: Num a => a -> a

「(* 2) x」を「(x * 2)」と書くのは、前に出してある演算子「*」を中置演算子としてxと2の間に置いただけ、と解釈すべきなのだろうか?
ここら辺が推測の域を出ていなくて、よく理解できない。
それに本書の説明では、まだカリー化の説明が登場していないので、2つの引数を取る中置演算子を1つの引数を取る関数の形に書き換えるテクニックは使えないはずだ。
途中に関数を置いて、2ステップでwhere句を除去する方法は良しとして、具体的な式の変形を示して欲しかった。
この謎を解決するには、

  1. 著者にメールなどで聞くか、
  2. 検索して調べるか、
  3. 今後の宿題としてもう少し学習が進んだ後にもう一度考えてみるか、
    しかないだろうか?

そこでHaskellのラムダ関数について、他の説明を参照して、知識の過不足、ズレを修正したいと思い検索してみた。

↓この連載記事は一度全部に目を通しておいた方がいいかも。

単純には、Haskellの問題じゃなくて、ラムダ関数(無名関数)の使い方の知識が不足しているのだろう。
高階関数の扱い方とかが何かズレているのかもしれない。

クイックチェック 3-3

練習問題

Q3-1

Q3-2


トップ   編集 凍結 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2019-09-16 (月) 00:56:58