はじめに
純粋関数型プログラミング言語であるHaskell
を参考に、以下の3つの構成で高階関数についてまとめていきます。
- カリー化関数
- 高階関数の実践
- ラムダ式
今回は、標準ライブラリで扱うことができる代表的な高階関数をいくつかまとめています。
高階関数
まずは、関数を引数で受け取る簡単な例を示します。以下のapplyTwice
は、受け取った関数を2回適用する関数です
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f x)
関数を引数にする場合はかっこで囲む必要があります。上の例では一つの引数を取り、同じ型の値を返す関数を引数として受け取っています。
引数である関数をx
に適用し、再びf
を適用しています。動作例を以下に示します。
Prelude> applyTwice (+3) 10
16
Prelude> applyTwice (3:) [2]
[3, 3, 2]
セクション1を使った部分適用を使っています。とても単純な例ですが、汎用性が高い関数ができてるのがわかるかと思います。
1 中間関数に関しても部分適用することができ、これを中間関数をセクションするといいます。中間関数をセクションするには(+3)
のように片側だけに値を置いて括弧で囲みます。
map
map
関数はリストに対して、引数の関数をすべての要素に適用し、新しい関数を返す関数です。実例を見るとわかりやすいので、定義を以下に示します。
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
第一引数は、a型の値を受け取りb型の値を返す関数となっています。そのため、第二引数のリストはa型の要素の必要があります。二行目のパターンマッチは、リストが空だった場合は空のリストを返すという意味です。_
はこの値を使用しない時に使う予約語で、使い捨ての変数を表しています。
(x:xs)
はリストの最初の要素とその他の要素に分けるという意味で、最初の要素に関数を適用しその他の要素に対して再帰的にmap
を適用しています。
Prelude> map (+2) [1, 2, 3, 4]
[3, 4, 5, 6]
Prelude> map (replicate 3) [1..3]
[[1,1,1],[2,2,2][3,3,3]]
このように、すべての要素に関数が適用されています。
zipWith
zipWith
は、二つのリストの各要素に関数を適用し1つのリストに結合する関数です。
こちらも実例を見るとわかりやすいので定義を以下に示します。
zipWith' :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith' _ [] _ = []
zipWith' _ _ [] = []
zipWith' f (x:xs) (y:ys) = f x y : zipWith' f xs ys
第一引数の関数はa型とb型の値からc型の値を返す関数となっています。そのため、第2引数のリストはa型の要素、第3引数のリストはb型の要素となっています。zipWith'
2の使用例は以下のようになります。
Prelude> zipWith' (+3) 10
16
Prelude> zipWith' (3:) [2]
[3, 3, 2]
2 関数名の最後についてるアポストロフィ(')は、特別な意味を持たない関数名の一部として有効な文字です。慣習的に正格版の関数を表したり、少し変更したバージョンの関数に似た名前をつけるために利用されます。今回は標準ライブラリのzipWith
関数を自作で定義しているために使用しています。
Swift
で高階プログラミング
Haskell
でいくつか高階関数の例を示しましたが、今度はSwift
で高階関数を扱っていきたいと思います。
高階関数の良いところの一つとして、汎用性のある関数が定義できるところです。
まず高階関数をつかっていない例として、各要素を二倍にして返す関数を定義してみます。
func double(_ array: [Int]) -> [Int] {
var result: [Int] = []
for x in array {
result.append( x * 2)
}
return result
}
double([3, 4]) // [6, 8]
この関数はあまり良いものとは言えません。Int
を3倍にしたい時や、Float
を扱いたい時などに変更が必要なためです。
これは高階関数とジェネリクスを使用することで解決できます。
標準ライブラリのmap
関数を自作で定義してみます。
func map<Element, T>(_ array: [Element], transform: (Element) -> T) -> [T] {
var result: [T] = []
for x in array {
result.append(transform(x))
}
return result
}
map([2, 3]) { $0 * 2 } // [4, 6]
map([8, 4]) { $0 / 2 } // [4, 2]
map(["Swit", "swif"]) { $0 + "t" } // ["Swift", "swift"]
標準ライブラリのmap
と同じような挙動になっていますね。このように、高階関数を扱うことで汎用性がある関数を定義できます。
同様にfilter
も高階関数をつかってこのような定義で実現できます。
func filter<Element>(_ array: [Element], includeElement: (Element) -> Bool) -> [Element] {
var result: [Element] = []
for x in array where includeElement(x) {
result.append( x)
}
return result
}
filter([3, 4, 5]) { $0 > 3 } //[4, 5]
カリー関数
今度は少し難易度を上げて、引数と返り値に関数を扱うカリー関数を定義してみます。
ジェネリクスを用いて以下のように定義します。
func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
return { x in
{ y in f(x, y) }
}
}
ぱっと見、何ができるかわからないような関数ですね。
このカリー関数を使用して、要素をカウント分繰り返した配列を返してみます。
let repeateString = curry { (count: Int, element: String) -> [String] in
Array(repeating: element, count: count)
}
let threeArray = repeateString(3)
threeArray("Swift") // ["Swift", "Swift", "Swift"]
threeArray("Haskell") // ["Haskell", "Haskell", "Haskell"]
Swift
でも関数の部分適用ができていますね。部分適用された関数を使い回すことができます。