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

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

草場代表
2020/11/29

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

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

レッスン4. 一級関数

レッスン4を読んだ後は

一級関数の定義を理解する
関数を他の関数の引数として使用する
関数からの抽象的な計算
関数を値として返す

がわかるようになります。

関数型プログラミング言語の機能の中で最も普及しているのは、ファーストクラス関数です。今日ではほとんどのプログラミング言語がこの考え方をサポートし、頻繁に使用しています。

JavaScriptでイベントハンドラを割り当てたり、Pythonなどの言語でカスタムソートロジックをソートメソッドに渡したりしたことがある人は、すでにファーストクラス関数を使ったことがあるでしょう。

AmazonやeBayなどの他のサイトで様々な商品の価格を比較するサイトを作りたいとします。必要なアイテムのURLを返す関数はすでに持っていますが、ページから価格を抽出する方法を決めるコードをサイトごとに書く必要があります。解決策としては、サイトごとにカスタム関数を作るという方法があります。

getAmazonPrice url
getEbayPrice url
getWalmartPrice url

HTMLから価格を抽出するロジックを完全に分離して、それを共通のgetPrice関数に渡す方法はあるでしょうか?

4.1. 引数としての関数

一級関数では、関数は引数として使われ、他の関数から値として返されます。これは、プログラミング言語が持つべき強力な機能です。これにより、コードから反復的な計算を抽象化することができ、最終的には他の関数を書く関数を書くことができるようになります。

数値 n が偶数の場合にインクリメントし、そうでない場合は変更されずに数値を返す ifEveninc 関数があるとします。

ifEvenInc n = if even n
then n + 1
else n

さらに2つの関数を考えます。

ifEvenDouble n = if even n
then n * 2
else n

ifEvenSquare n = if even n
then n^2
else n

唯一の違いは、インクリメント、ダブリング、スクエアリングの動作です。抽象化できる計算の一般的なパターンがあります。そのために必要なのは、関数を引数に渡して目的の動作を実行できることです。

関数と数値を引数に取る関数 ifEvenを考えます。

ifEven myFunction x = if even x
then myFunction x
else x

インクリメント、ダブルリング、スクエアリングの動作を3つの別々の関数に抽象化することもできます。

inc n = n + 1
double n = n*2
square n = n^2

一級関数の力を借りて、これまでの定義を再現する方法を見てみましょう。

ifEvenInc n = ifEven inc n
ifEvenDouble n = ifEven double n
ifEvenSquare n = ifEven square n

これで、ifEvenCubeやifEvenNegateのような新しい関数を簡単に追加することができるようになりました。

関数と演算子の優先順位
すでに関数と演算子の例を見てきました。例えば、 inc は関数、+ は演算子です。Haskellのコードを書く上で重要なことは、関数は常に演算子の前に評価されるということです。GHCiのこの例を見てみましょう。

GHCi> 1 + 2 * 3
7

1 +をincに置き換えるとどうなるでしょうか?

GHCi> inc 2 * 3
9

この結果は、関数が常に演算子よりも優先されるため、異なる結果となります。これは複数引数の関数でも同じことが言えます。

GHCi> add x y = x + y
GHCi> add 1 2 * 3
9

これにより、コードの中で不要な括弧を大量に使用することを避けることができます。

4.1.1. 引数としてのラムダ関数

関数に渡すコードを素早く追加するためにラムダ関数を使うこともできます。値を2倍にしたい場合は、ラムダ関数を素早くまとめることができます。

GHCi> ifEven (\x -> x*2) 6
12

4.1.2. 例-カスタムソート

名字と名前のリストがあるとします。それぞれの名前はタプルとして表現されています。タプルはリストのような型で、複数の型を含むことができ、サイズは固定されています。

author = ("Will","Kurt")

2つの項目のタプルには、2つの便利な関数fstとsndがあり、それぞれタプルの1番目と2番目の要素にアクセスします。

GHCi> fst author
"Will"
GHCi> snd author
"Kurt"

タプルのリストとして表される名前のセットがあるとします。

names = [("Ian", "Curtis"),
("Bernard","Sumner"),
("Peter", "Hook"),
("Stephen","Morris")]

Haskellにはソート機能が組み込まれています。これを使うには、まずData.Listモジュールをインポートする必要があります。

import Data.List

Haskell の sort がこれらのタプルをどのようにソートするかをうまく推測していることがわかります。

GHCi> sort names
[("Bernard","Sumner"),("Ian", "Curtis"),("Peter", "Hook"),
("Stephen","Morris")]

より詳細にソートするために、Data.Listモジュールに含まれるHaskellのsortBy関数を使うことができます。

sortByには、タプル名のうちの2つの要素を比較する別の関数を指定する必要があります。

そのために、関数 compareLastNames を書きます。

この関数はname1とname2の2つの引数を取り、GT、LT、またはEQを返します。GT、LT、EQは、大きいより、小さいより、等しいを表す特殊な値です。多くのプログラミング言語では、True もしくは False、もしくは 1、-1、もしくは 0 を返します。

compareLastNames name1 name2 = if lastName1 > lastName2
then GT
else if lastName1 < lastName2
then LT
else EQ
where lastName1 = snd name1
lastName2 = snd name2

これでカスタムソートでsortByを使うことができるようになりました。

GHCi> sortBy compareLastNames names
[("Ian", "Curtis"),("Peter", "Hook"),("Stephen","Morris),
("Bernard","Sumner")]

4.2. 戻り関数

関数を引数として渡すことは、ファーストクラスの関数を値として持つことの意味の半分に過ぎません。関数もまた値を返しますので、真にファーストクラスの関数のためには、関数が他の関数を返さなければならないことがあります。なぜ関数を返したいと思うのか、ということです。

理由の一つは、他のパラメータに基づいて特定の関数をディスパッチしたいということです。

会員にニュースレターを送る必要があるとします。郵便局は3つの都市にあります。サンフランシスコ、リノ、ニューヨークです。

私書箱1234, サンフランシスコ, CA, 94111
私書箱789, ニューヨーク, ニューヨーク, NY, 10013
私書箱456, リノ, NV, 89523

名前のタプルとオフィスの場所を受け取り、あなたのために郵送先住所をまとめる関数を作る必要があります。この関数の最初のパスは次のようになります。

addressLetter name location = nameText ++ " - " ++location
where nameText = (fst name) ++ " " ++ (snd name)]

この関数を使用するには、名前のタプルとフルアドレスを渡さなければなりません。

GHCi> addressLetter ("Bob","Smith") "PO Box 1234 - San Francisco, CA, 94111"
"Bob Smith - PO Box 1234 - San Francisco, CA, 94111"

アドレスを追跡するために変数を簡単に使用することができます。これでニュースレターの配信準備は完了です。

地域のオフィスからいくつかの苦情や要望が出てきました。サンフランシスコでは、アルファベットの「L」以降で始まる苗字のメンバーのために、新しい住所を追加しました。

私書箱1010, San Francisco, CA, 94109。

ニューヨークでは、ハイフンではなくコロンの後に名前をつけてほしいとのことです。リノは機密性を高めるために名字だけを使用することを望んでいます。オフィスごとに異なる機能を必要としていることは明らかです。

sfOffice name = if lastName < "L"
then nameText
++ " - PO Box 1234 - San Francisco, CA, 94111"
else nameText
++ " - PO Box 1010 - San Francisco, CA, 94109"
where lastName = snd name
nameText = (fst name) ++ " " ++ lastName

nyOffice name = nameText ++ ": PO Box 789 - New York, NY, 10013"
where nameText = (fst name) ++ " " ++ (snd name)
renoOffice name = nameText ++ " - PO Box 456 - Reno, NV 89523"
where nameText = snd name

では、この3つの関数をaddressLetterでどのように使うべきか?addressLetter を書き換えます。

場所の文字列を受け取り、あなたのために適切な関数をディスパッチする別の関数です。getLocationFunctionと呼ばれる新しい関数を作成します。この関数は1つの文字列を受け取り、正しい関数をディスパッチします。

getLocationFunction location = case location of 1
"ny" -> nyOffice 2
"sf" -> sfOffice 3
"reno" -> renoOffice 4
_ -> (\name -> (fst name) ++ " " ++ (snd name)) 5

1 :location の値を見ます。
2: locationがnyの場合、nyOfficeを返します。
3 :ロケーションがsfの場合、sfOfficeを返します。
4 :場所がRenoの場合、RenoOfficeを返します。
5 :他の何かであれば(_はワイルドカード)、一般的な解を返します。

Haskell では _ はワイルドカードとしてよく使われます。コードのユーザが無効な場所を渡した場合、名前のタプルを文字列にするラムダ関数を作成します。最後に、次のように addressLetter を書き換えることができます。

addressLetter name location = locationFunction name
where locationFunction = getLocationFunction location

GHCi> addressLetter ("Bob","Smith") "ny"
"Bob Smith: PO Box 789 - New York, NY, 10013"

GHCi> addressLetter ("Bob","Jones") "ny"
"Bob Jones: PO Box 789 - New York, NY, 10013"

GHCi> addressLetter ("Samantha","Smith") "sf"
"Samantha Smith - PO Box 1010 - San Francisco, CA, 94109"

GHCi> addressLetter ("Bob","Smith") "reno"
"Smith - PO Box 456 - Reno, NV 89523"

GHCi> addressLetter ("Bob","Smith") "la"
"Bob Smith"

これで、住所を生成するために必要な各関数を分離したので、各オフィスからの問い合わせに応じて新しいルールを簡単に追加することができます。

まとめ

ファーストクラス関数について説明しました。

ファーストクラス関数では、関数を引数として渡したり、値として返したりすることができます。ファーストクラス関数は信じられないほど強力なツールです。ファーストクラス関数の威力は、ほとんどの最新のプログラミング言語で広く採用されていることからも証明されています。

 

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