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

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

草場代表
2020/11/27

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

Haskell触ります。「Get Programming with Haskell」についてです。まずは、ユニット1. 関数型プログラミングの基礎についてです。

ユニット1. 関数型プログラミングの基礎

プログラミングを理解するには、2つの方法があります。
1つ目は、プログラマーがコンピュータをある方向に動作させるために一連の命令をコンピュータに提供するという考え方です。このモデルでは、プログラマはコンピュータの設計に結び付けられています。

このタイプのプログラミングでは、コンピュータは入力を受け取り、メモリにアクセスし、処理装置に命令を送り、最終的にユーザに出力を提供する装置です。このようなコンピュータのモデルは、有名な数学者で物理学者のジョン・フォン・ノイマンにちなんで、フォン・ノイマン・アーキテクチャと呼ばれています。

C言語が代表例です。

C言語のプログラムは、オペレーティングシステムが制御する標準入力からデータを取り込み、必要な値を手動で管理しなければならないことが多い物理メモリに格納・取得し、特定のメモリブロックへのポインタの扱いを要求し、最終的にOSが制御する標準出力を介してすべての出力を返すという仕組みになっている。C言語のプログラムを書くとき、プログラマは目の前のコンピュータの物理的なアーキテクチャと同じくらい、目の前の問題を理解していなければなりません。

第2の理解の仕方は、関数型プログラミングです。関数型

プログラミングの基礎となるのは、特定の実装を超越した抽象的で数学的な計算の概念です。これにより、問題を記述するだけで解決することが多いプログラミングの手法につながっています。コンピュータではなく計算に焦点を当てることで、関数型プログラミングでは、プログラマーは強力な抽象化にアクセスすることができ、多くの困難な問題をはるかに簡単に解決することができます。

しかし、その代償として、始めることが非常に困難になることがあります。

レッスン2. 関数と関数型プログラミング

レッスン2を読んだ後は

関数型プログラミングの一般的な考え方を理解する
Haskellで簡単な関数を定義する
Haskellでの変数の宣言
関数型プログラミングの利点を説明する

関数型プログラミングとは何か?

あなたは友達とピザを食べに出かけています。メニューには3種類のサイズのピザパイがあり、値段も3種類あります。

18インチで20ドル
16インチで15ドル
12インチで10ドル

2.1. 関数

関数とは何か?数学では、f(x) = y、つまりパラメータ x を取り、値 y にマップする関数 f があるということを意味します。xはが1つのyに対応します。Haskellでは,関数は数学とまったく同じように動作します。
以下の単純な関数は単一の引数 x を取り、この引数をそのまま返します。

Haskellでは値を返すことを指定する必要がないことに注意してください。Haskellでは、関数は値を返さなければならないので、明示的に指定する必要はありません。簡単な関数をGHCiにロードして、その動作を確認することができます。関数をロードするには、関数をファイルに入れて、GHCiの中で :load <filename> を使うだけです。

GHCi> simple^2
2
GHCi> simple "dog"
"dog"

Haskellのすべての関数は、数学の関数のように振る舞うための3つのルールに従っています。

すべての関数は引数を取らなければなりません。
すべての関数は値を返さなければなりません。
同じ引数で関数を呼び出す場合は、常に同じ値を返さなければなりません。

3番目のルールは、関数の基本的な数学的定義の一部です。同じ引数は常に同じ結果を返さなければならないというルールをプログラミング言語で関数に適用した場合、これを参照透過性と呼びます。

2.2. 関数プログラミング

関数がxの束からyの束への写像に過ぎないとしたら、それがプログラミングと何の関係があるのでしょうか?

1930年代にアロンゾ・チャーチという数学者が、関数と変数(xsとys)だけを使った論理体系を作ろうとしました。この論理系はラムダ微積分と呼ばれています。ラムダ微積分では、すべてのものを関数として表現し、真と偽は関数であり、すべての整数も関数として表現することができる。チャーチの目標は、当初、集合論という数学の分野におけるいくつかの問題を解決することでした。残念ながら、ラムダ微積分はこれらの問題を解決することはできませんでしたが、チャーチの研究からはもっと興味深いものが生まれました。それは、ラムダ微積分は、チューリングマシンに相当する普遍的な計算モデルを可能にすることが判明したのです。

チューリング・マシンは、デジタル・コンピュータだけでなく、あらゆる可能性のあるコンピュータで、何が計算できて何ができないのかを推論できるので便利です。コンピュータ科学者がそれぞれの計算システムがチューリング・マシンをシミュレートできれば、計算システム間の同等性を示すことができます。

あなたが使っているほとんどのプログラミング言語は、工学的には素晴らしいものですが、プログラムがどのように振る舞うかについては、ほとんど保証がありません。数学的な基盤を持つHaskellは、あなたが書いたコードからバグやエラーのクラス全体を取り除くことができます。プログラミング言語の最先端の研究では、プログラムが期待通りに動作することを数学的に証明する方法が実験されています。さらに、ほとんどのプログラミング言語の設計の非数学的な性質は、使用できる抽象化が言語の工学的な決定によって制限されていることを意味します。もしあなたが数学のプログラミングができれば、あなたのコードを証明することができると同時に、数学が可能にするほぼ無限の抽象化にアクセスすることができるようになります。これが関数型プログラミングの目的です: 数学の力を使いやすい方法でプログラマにもたらすことです。

2.3. 関数型プログラミングの実際の価値

すべての関数は値を受け取って返さなければならず、同じ引数に対しては常に同じ値を返さなければならないという単純なルールのため、Haskellは安全なプログラミング言語です。プログラムが常にあなたの期待通りに動作し、その動作について簡単に推論できる場合、プログラムは安全です。

安全なプログラミング言語とは、プログラムが期待通りに動作するように強制する言語です。

例えば、以下のようなコードの行に出くわしたとします。

tick()
if(timeToReset){
  reset()
}

関数は引数を取らず、値を返しません。これらの関数は何をしているのか?、そしてHaskellの関数とどう違うのか?

関数に引数を渡していない場合は、環境内の値にアクセスしているはずで、値を返さない場合は、環境内の値を変更しているはずです。プログラミング環境で値を変更するということは、プログラムの状態を変更していることになります。状態を変更することでコードに副作用が発生し、その副作用によってコードの推論が難しくなり、安全ではなくなってしまいます。

tick と reset の両方がグローバル変数にアクセスしている可能性がありますが、これはどのプログラミング言語でも設計上の問題とみなされます。これを見るために、値の集まりである myList を見て、組み込みの機能を使ってそれを逆にしてみます。次のコードは Python、Ruby、JavaScript で有効です。

myList = [1,2,3]
myList.reverse()
newList = myList.reverse()

さて、newListの値はどうなるか?これはRuby、Python、JavaScriptで有効なプログラムなので、newListの値が同じであると考えるのが妥当です。以下に3つの言語の答えを示します。

Ruby -> [3,2,1]
Python -> None
JavaScript -> [1,2,3]

ここでのRubyのコードはHaskellのように振る舞いますが、副作用はありません。

ここでは、参照透過性の価値を見ることができます。Haskellでは、各関数がどのような効果を持っているかを常に見ることができます。

ソースコードを見なければ、どの値を使っているのか、あるいはどのくらいの数の値を使って変更しているのかを正確に知ることはできません。Haskellは関数に副作用を持たせることを許さないので、Haskellの関数はすべて引数を取り、値を返さなければなりません。Haskellの関数が常に値を返さなければ、プログラムの隠れた状態を変更しなければなりません。引数を取らなかった場合は、隠された状態にアクセスしなければならず、それはもはや透明ではないことを意味します。

Haskellの関数のこの特性により、コードの予測が容易になります。

2.3.1. 変数

以下は、変数 x に 2 を代入しています。

x = 2

Haskell の変数の唯一の注意点は、実際には全く変数ではないということです!次のビットの Haskell をコンパイルしようとすると、次のリストに示すようにエラーが出ます。次のリストで示されるように、次のビットの Haskell をコンパイルしようとすると、エラーが発生します。

リスト2.4. 変数は変数ではない!

x = 2
x = 3           1

1: x の値を変更するため、コンパイルできません。
変数を変更できないことは、参照透過性にも関係しています。

変数利点は、コードを明確にし、繰り返しを避けることです。calcChangeという、2つの引数(いくら借りているか、いくら与えられているか)をとる関数を考えます。十分な金額が与えられていれば、その差額を返し、十分な金額が与えられていない場合は0を返します。これを書くには、

calcChange owed given = if given - owed > 0
                        then given - owed
                        else 0

Haskellでは、where、を使うことでより明確になります。

calcChange owed given = if change > 0
                        then change
                        else 0
  where change = given – owed               1

1: 与えられた – owed は一度だけ計算され、変更に代入されます。

最初に興味深いと思われるのは、where節が変数を書くときに使われる通常の順序を逆にしていることです。ほとんどのプログラミング言語では、変数は使用する前に宣言されます。ほとんどのプログラミング言語におけるこの慣習は、部分的には状態を変更できることの副産物です。変数の順序が重要なのは、変数を代入した後であれば、いつでも値を再代入することができるからです。Haskellでは、参照透過性があるため、これは問題ではありません。Haskellのアプローチでは、アルゴリズムを読めば、その意図がすぐに明らかになるという可読性の向上もあります。


2.3.2. 変数である変数

GHCiで作業するときは、変数の再割り当てが許可されています。例を示します。

GHCi> x = 7
GHCi> x
7
GHCi> x  = [1,2,3]
GHCi> x
[1,2,3]

GHC のバージョン 8 より前のバージョンでは、GHCi の変数は Haskell の他の変数とは異なるものであることを示すために let キーワードを前置する必要がありました。GHCiではletを使って変数を定義することができます。

GHCi> let x = 7
GHCi> x
7

また、一行関数も同じように定義できます。

GHCi> let f x = x^2
GHCi> f 8
64

Haskellの他のいくつかの特別なコンテキストでは、このようにletが使われているのを目にすることがあります。

サマリー

関数型プログラミングとHaskellでの基本的な関数の書き方を紹介しました。関数型プログラミングでは、関数の動作に制限があります。これらの制限は以下の通りです。

関数は常に引数を取らなければなりません。
関数は常に値を返さなければならない。
同じ引数で同じ関数を呼び出しても、常に同じ結果を返さなければならない。

このスタイルでコードを書くことの大きな利点は、プログラムの推論がはるかに簡単になり、予測可能な動作をするようになることです。

 

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