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

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

入門Haskellプログラミング

  入門Haskellプログラミング
Will Kurt
翔泳社
2019-07-31
¥ 4,104

LESSON 3

  • Haskellでのラムダ関数の記述
  • ラムダ式を使った特別な関数の定義
  • レキシカルスコープ
  • ラムダ関数によるスコープの作成

3.1 ラムダ関数

キーワード
  • 「λ」(ラムダ:ギリシャ語の小文字。アルファベットのエル「l」に相当)
  • 「\」(バックスラッシュ)
  • 「->」(矢印)

書式

\x -> x
  • 最初にバックスラッシュ「\」を書く。(Windowsパソコンの場合、バックスラッシュ記号が円マーク「¥」で表示される場合もある)
  • 続いて、引数を書く。上記の書式では、左側の「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句

キーワード
  • 補助関数

(参考)

補助関数とは、関数定義の内部でのみ使用する部分的な関数のことです。
補助関数を作成することで、複雑で分かりにくいプログラムの構造を簡潔にして可読性を向上することができます。
また、処理毎に分割することはプログラムを再利用しやすく、保守性のあるものにします。
なお、補助関数を内包する親となる関数は「最上位関数」と呼ばれます。
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を使っておくことにしておきたいと思います。

補足

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

ラムダ関数

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

  • Haskell-無名関数 CapmNetwork

    無名関数とは
    無名関数とは、一時的に使用するために使い捨てる前提で生成する、名前の無い関数です。
    Haskellではラムダ式(ラムダ関数)を記述することで無名関数を定義することが出来ます。

    ラムダ関数(lambda calculus)とは
    ラムダ関数とは、文字ラムダ (λ) を使った式によって表記する関数です。
    ラムダ式で記述された関数は、一時的な使い捨ての関数として利用することができます。
    なお、ラムダ関数は、匿名関数(無名関数)とも呼ばれます。

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

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

  • ラムダ計算とは (ラムダケイサンとは) [単語記事] - ニコニコ大百科
    https://dic.nicovideo.jp/a/%E3%83%A9%E3%83%A0%E3%83%80%E8%A8%88%E7%AE%97

    名前の由来
    ラムダ式に用いられる記号 λ に由来するわけだが、この λ 自身の由来については2説あり、真偽についてはあまりはっきりしていない。

    由来がある説
    Rosserという人が1984年に以下のように報告している。
    Russell と Whiteheadが、関数を抽象化するときの記号に「^x(HTMLの仕様上表記できないが、âのaをxにしたもの)」を用いていた。Alonzo Churchは、この表記法にちなんで、「∧x」という記号を使用するようになった。その後印刷しやすいように「∧」の代わりに「λ」を使用するようになった(ちなみにλの大文字はΛ(≠∧)である)。

    由来なんかない説
    Alonzo Church自身は後年「とにかく記号が必要だったからたまたまλを選んだ」と語っており、この話を信じるなら特に由来はないということになる。一般には紙面で報告された「由来がある説」の方が信頼性が高いということになるが、紙面とはいえ他人がした報告である。一方こちらはAlonzo Church本人の言葉であり、そう聞いた人が1人だけというわけでなく2人いるということなので、こちらもそれなりに信憑性が高い。

  • Island Life - λの起源、2つの説
    http://blog.practical-scheme.net/shiro/20160826-origin-of-lambda

    λ算法はなぜλなのかについては、次の説が有名である。

    Alonzo Churchがλ算法を考えた時、RusselとWhiteheadが Principia Mathematicaで束縛変数を表すのにカレット^を 使っているのに倣ったが、1行でタイプする都合上カレットを前に出し ^x f(x) とし、それが λx f(x)となった。

    どうやら、Church自身が両方の説(^説と偶然説)を語っているようだ。

    可能性としては、

    • λの選択は偶然だったが、Harald Dicksonへの手紙ではジョークのつもりで もっともらしい話をでっち上げたらそれが広まってしまった
    • Principia Mathematicaからインスピレーションを得たといえばそうなのだが、 記号の起源なんてどうでもいいと思っていたので後の方ではただ偶然ということにしておいた
      あたりだろうか。

^(キャレット)記号

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

  • サーカムフレックス - Wikipedia

    サーカムフレックス(英語: circumflex)または曲折アクセント(きょくせつアクセント)は欧文用の「山」型の記号で、フランス語、ポルトガル語、ベトナム語、ルーマニア語、エスペラント、日本語のローマ字などで用いられるダイアクリティカルマーク(発音区別符号)の一種。
    別名、キャレット (caret)、ハット記号 (hat [symbol])。
    ただしUnicodeでは、「キャレット」は別の文字 U+028C の名称になっている(そちらが原義)。
    有間隔のサーカムフレックス「ˆ」はASCIIに含まれ、他の文字を修飾しない独立した記号として使われる。

  • ダイアクリティカルマーク - Wikipedia

    ダイアクリティカルマーク(英語: diacritical mark)または発音区別符号(はつおんくべつふごう)は、ラテン文字等の文字で、同じ字形の文字であるが、発音が区別されるべき場合に文字に付される記号のこと。

  • キャレット - Wikipedia

    キャレット・脱字符号 (英: caret) は、校正で、脱字の挿入を指示する記号。挿入位置の下(縦書きでは横)に山形を書き、その下に挿入したい文字を書く。なお、単なる脱字でなく長い語句を挿入したいときは、キャレットの代わりにブレースを使う。
    コンピュータの文字入力画面で、入力文字の挿入位置、つまり、カーソルとしても使われる。言葉どおりキャレット型のもののほか、点滅する縦棒などさまざまな形式のものがある。また、制御文字を表現するのに、キャレットとアルファベットを使ったキャレット記法が用いられる。
    また、ASCIIの5E「^」(サーカムフレックス)や、数学記号で論理積や外積を表す「∧」(ウェッジ)を、字形の類似からキャレットと呼ぶこともある。

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

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

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

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

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

クイックチェック

クイックチェック 3-1

クイックチェック 3-2

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

  1
  2
doubleDouble x = dubs*2
  where dubs = x*2

  1
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

ところが、

  1
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倍にする関数として扱われるはずだ。

  1
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を入れる意味、思考の過程が分からないのであった。

  1
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 (1677d)