未来を創る、テックコミュニティー

代表技術通信~Get Programming with Haskell④

草場代表
2020/11/28

こんばんは。代表の草場です。

Haskell触ります。「Get Programming with Haskell」についてです。次は、レッスン3です。

レッスン3. ラムダ関数と語彙範囲

レッスン3を読んだ後は

Haskellでラムダ関数を書く
アドホック関数の定義にラムダ関数を使用する
語彙的範囲を理解する
ラムダ関数でスコープを作成

が出来るようになります。

関数型プログラミングの中で最も基本的な概念の一つであるラムダ関数について学びます。ラムダ関数とは、名前のない関数のことです。

GHCiで、3つの値の和の2乗と3つの値の2乗の和の差を計算として、これを手書きで書き出すと、

GHCi> (4 + 10 + 22)^2 – (4^2 + 10^2 + 22^2)

しかし、これはタイプミスで式がエラーになります。もう少しすっきりする方法はないものでしょうか?

3.1. ラムダ関数

関数型プログラミングの最も基本的な概念の1つは、ラムダ関数と呼ばれる名前のない関数です。ラムダ関数は、しばしばギリシャ語の小文字λを使って呼ばれます。ラムダ関数の別の一般的な名前は無名関数です。ラムダ関数を使用して、レッスン2の単純な関数を名前なしで再定義することができます。

ラムダ関数は、値を受け取って値を返す、それだけの最低限の関数です。一番簡単なのは引数を渡すことです。

GHCi> (\x -> x) 4
4
GHCi> (\x -> x) "hi"
hi
GHCi> (\x -> x) [1,2,3]
[1,2,3]

ラムダ式を使うたびに、それを再定義しなければなりません。ラムダ関数は、短期間しか存在しないように設計されています。

3.2. 独自の WHERE CLAUSE を書く

ラムダ関数がどれだけ強力なものであるかを示すために、Haskellのwhere節を削除して、何もないところから再構築できるかどうかの実験します。これまでのところ、関数の中で変数を格納する唯一の方法はwhereです。

2つの数値を受け取り、値の2乗の和 (x^2 + y^2) または和の2乗 ((x + y)^2) のどちらか大きい方を返す関数を考えます。以下は、whereを使ったバージョンです。

sumSquareOrSquareSum x y = if sumSquare > squareSum
then sumSquare
else squareSum
where sumSquare = x^2 + y^2
squareSum = (x+y)^2

sumSquareOrSquareSum では、コードを読みやすくし、計算量を減らすために where を使用しています。where がなければ、単に変数を置き換えることができますが、そうすると計算量が2倍になり、コードは醜いものになってしまいます。

sumSquareOrSquareSum x y = if (x^2 + y^2) > ((x+y)^2)
then (x^2 + y^2)
else (x+y)^2

変数がない場合の解決策の1つは、関数を2つのステップに分割することです。

body という名前の関数で sumSquareOrSquareSum の主な比較部分を処理し、新しい sumSquareOrSquareSum で sumSquare と squareSum を計算して body に渡します。以下は body のコードです。

body sumSquare squareSum = if sumSquare > squareSum
then sumSquare
else squareSum

そして sumSquareOrSquareSum は sumSquare と squareSum を計算して body に渡さなければなりません。

sumSquareOrSquareSum x y = body (x^2 + y^2) ((x+y)^2)

これはとてもシンプルな関数で、途中のステップが必要ないのが良いです。名前のついたボディ関数をどうにかして取り除きたいので、ラムダ関数には最適です。まず、bodyのラムダ関数を見ます。

body = (\sumSquare squareSum ->
if sumSquare > squareSum
then sumSquare
else squareSum)

重要なことは、変数のアイデアをゼロから実装したということです。

3.3. ラムダからレットへ: あなただけの可変変数を作ろう!

ラムダ関数は式であり、自己完結型のコードの塊です。

Haskellにはlet式と呼ばれるwhere句に代わるものがあります。let 式を使うと、where 句の可読性とラムダ関数のパワーを組み合わせることができます。

letを使うかwhereを使うかは、Haskellの大部分の時間のスタイルの問題です。

以下の例で、読みやすさのために生のラムダ式の代わりに let 式を使用し、変数を上書きすることが意可能であることを示すために、次のリストでは変数 x を受け取り、その値を 3 回上書きする関数の上書きを示しています。

overwrite x = let x = 2
in
let x = 3
in
let x = 4
in
x

これ自体は何の役にも立たない関数ですが、GHCiで変数を再定義する方法を思い出すはずです。

GHCi> let x = 2
GHCi> x
2
GHCi> let x = 3
GHCi> x
3

上書き関数は、GHCiがどのように変数の再定義を可能にし、関数型プログラミングのルールに関して「ズルをしない」ことを可能にするかについての洞察を提供します。

3.4. 実用的なLAMBDA関数とLEXICAL SCOPE

JavaScript はラムダ関数を強力にサポートしています。JavaScript の \x -> x に相当するものは以下の通りです。

function(x){
return x;
}

JavaScriptの最大の欠陥の1つは、JavaScriptには名前空間やモジュールが実装されていないことです。関数 libraryAdd から始めます。

var libraryAdd = function(a,b){
c = a + b; 1
return c;
}

1 :ここで、JavaScript の var キーワードを使用するのを忘れてしまい、誤ってグローバル変数を作成してしまいました。変数 c が誤ってグローバル変数として宣言されています。これがどのように問題を引き起こすのか、例を挙げてみましょう。

var a = 2;
var b = 3;
var c = a + b;
var d = libraryAdd(10,20); 1
console.log(c); 2

1 :内部的には、この関数はグローバル変数cにアクセスしますが、それを知る方法はありません。
2 :この値は30であり、5ではありません。

あなたはすべて正しく実行しましたが、 libraryAdd を呼び出した後、変数 c は 30 になりました。これは、JavaScriptには名前空間が存在しないために発生します。残念ながら、見つけたのは var c です。誰か他の人の JavaScript コードを深く掘り下げない限り、このバグを解明することはできないでしょう。

この問題を解決するために、JavaScript の開発者はラムダ関数を使いました。コードをラムダ関数で包み、すぐにその関数を呼び出すことで、コードを安全に保つことができます。このパターンは、すぐに呼び出される関数式 (IIFE) と呼ばれています。IIFE を使うと、あなたのコードは次のようになります。

(function(){ 1
var a = 2;
var b = 3;
var c = a + b;
var d = libraryAdd(10,20); 2
console.log(c); 3
})()

1 ラムダ関数の定義
2 この危険な機能は今すぐには手を出せない
3 5の正しい値

IIFE は where 文を置き換える例と全く同じ原理で動作します。名前の有無にかかわらず、新しい関数を作成するときはいつでも、新しいスコープを作成します。変数が使用されると、プログラムは最も近いスコープを探します。この特定のタイプの変数検索は語彙スコープと呼ばれています。Haskell も JavaScript も辞書的スコープを使用しているので、IIFE とラムダ関数の変数が似たような動作をするのはそのためです。

GHCi> add1 1
5
GHCi> add2 1
4
GHCi> add3 1
3

名前のない関数を使用してその場でスコープを作成できることは、ラムダ関数を使用してより強力なことを行うために不可欠なツールです。

まとめ

ラムダ関数について学びました。

ラムダ関数とは、名前のない関数というシンプルな概念です。しかし、ラムダ関数は関数型プログラミングの基礎となるものです。関数型プログラミングの理論的なコーナーとしての役割の他に、実用的な利点もあります。最も明白な利点は、ラムダ関数を使えば、その場で簡単に関数を書くことができることです。ラムダ関数のさらに強力な特徴は、必要に応じてスコープを作成できることです。

 

この記事を書いた人
草場代表
エディター