書いて覚えるSwift入門

第1回関数型プログラミングを試す

関数という名の(定|変)

連載第1回目(もちろん0から数えています)の今回は、前回予告どおり関数型言語としてのSwiftを見ていきます。ところで「関数型言語」とはいったいなんでしょう。実のところあいまいな用語ではあるのですが、⁠最低限文化的な関数型」の関数は第一級オブジェクトであることに異議のある読者はあまりいらっしゃらないと思います。⁠第一級オブジェクトってなんぞや」おさらいしてみるとこういう感じでしょうか。

  • 変数に代入できる
  • 関数の引数にできる
  • 関数を返す関数が書ける

JavaScript、Lua、Perl、Python、PHP、Rubyといった今日日のスクリプト言語はその意味において「最低限文化的な関数型言語」の定義を満たしています。一例としてJavaScriptを見てみましょうか。前回のFizzBuzzを普通に書くとこんな感じでしょうかリスト1⁠。

リスト1 FizzBuzzの例
var fizzbuzz = function(n) {
    if (n % 15 == 0) { return "FizzBuzz"; }
    if (n % 5 == 0) { return "Buzz"; }
    if (n % 3 == 0) { return "Fizz"; }
    return n;
}
for (var i = 1; i <= 100; i++) {
     console.log(fizzbuzz(i))
}

見てのとおり、fizzbuzzは変数名で、そこに関数を代入しています。Swiftではどうでしょうか? こうかな?

var fizzubzz = func(n:Int)->String {
    if n % 15 == 0 { return "FizzBuzz" }
    if n % 5 == 0 { return "Buzz" }
    if n % 3 == 0 { return "Fizz" }
    return String(n)
}

いいえ、ちょっと違います。正しくはこうです。

var fizzbuzz = { (n:Int)->String in
    if n % 15 == 0 { return "FizzBuzz" }
    if n % 5 == 0 { return "Buzz" }
    if n % 3 == 0 { return "Fizz" }
    return String(n)
}

あるいはこうです。

var fizzbuzz:(Int)->String = { n in
    if n % 15 == 0 { return "FizzBuzz" }
    if n % 5 == 0 { return "Buzz" }
    if n % 3 == 0 { return "Fizz" }
    return String(n)
}

まとめるとこう。

funcは不要
  • { 引数 in 定義 }という形をしている
型指定は必要
  • →前者は{型定義付き引数 in 定義}
  • →後者は変数名:型 = { 引数名 in 定義 }

変数指定を{}の外ではなく中でやるあたり、JavaScriptよりむしろRubyっぽいですね。実はもっとRubyっぽいことをこれから見ていきます。FizzBuzzをちょっと変えて、⁠1から100に対応した結果を出力」するのではなく、⁠1から100まで入った配列をFizzBuzz変換」することを考えてみましょう。JavaScriptではこうかなリスト2⁠。

リスト2 JavaScriptの場合
function range(start, end) { // ないので作る
    var ret = [];
    for (var i = start; i <= end; i++) ret.push(i);
    return ret;
}
var a = range(1, 100).map(fizzbuzz)
console.log(a)

ここで、fizzbuzz()を無名化するとこうなります。

var a = range(1, 100).map(function(n) {
    if (n % 15 == 0) { return "FizzBuzz"; }
    if (n % 5 == 0) { return "Buzz"; }
    if (n % 3 == 0) { return "Fizz"; }
    return n;
}))

動くことは動きますが、()の中に入ったfunctionはなんとも不格好です。Rubyならどうでしょうか?

a = (1..100).map { ¦n¦
    if n % 15 == 0 then "FizzBuzz"
    elsif n % 5 == 0 then "Buzz"
    elsif n % 3 == 0 then "Fizz"
    else n.to_s
    end
}
p a

「メソッドの最後の引数がブロックの場合、()の外に書いてよい」というルールのおかげでずいぶんとエレガントです。それではSwiftでは?

var a = (1...100).map { n in
    if n % 15 == 0 { return "FizzBuzz" }
    if n % 5 == 0 { return "Buzz" }
    if n % 3 == 0 { return "Fizz" }
    return String(n)
}
println(a)

見てのとおり、mapの後の無名関数を()でくくらなくても動きました。無名関数の引数と戻り値のほうも指定していません。このあたりは、Swiftの父の1人であるChris Lattnerもホームページ「アイデアを拝借した」と率直に答えているとおりです。

しかし、Lattnerが言っていない、拝借されたアイデアがもう1つあります。コードで見てみましょうリスト3⁠。

リスト3 Rubyのアイデアを拝借
var a = (1...100).map {
    if $0 % 15 == 0 { return "FizzBuzz" }
    if $0 % 5 == 0 { return "Buzz" }
    if $0 % 3 == 0 { return "Fizz" }
    return String($0)
}
println(a)

なんと、inが消えてしまいました。その代わりnがあった位置に$0という未宣言の変数が存在します。この変数のことをプレイスホルダー(placeholder)変数というのですが、$0が最初の引数、$1が次の引数といった具合です。これ、どう見てもPerl 6のアイデアなんですが……。

{}は全部関数!

ここで今まで見てきたSwiftのコードの{}をじーっとよく見てみてください。何か見えてきませんか? ヒントを1つ。ifの後ろの{}は、Swiftでは省略不可能です。ifの後ろの条件には()がないのに……。

そう。Swiftでは、意味論的(semantically)には{}ブロックで、つまり関数なのです!

本当かどうか、確かめてみましょう。どうやって?――ifを再発明して!リスト4

リスト4 ifの再発明
func IF(PRED:()->Bool,THEN:()->(),ELSE:()->()) {
     [true:THEN, false:ELSE][PRED()]!()
}

func fact(n:Int)->Int {
    var ret:Int!
    IF( {n <= 1},
        { ret = 1 },
        { ret = n * fact(n - 1)}
    )
    return ret
}

println(fact(10))
図1 ifの再発明
図1 ifの再発明

動いてます図1⁠。動いちゃってます。IFは関数で、しかも中に条件分岐がいっさいないのに。それにしても、関数IFはずいぶん奇妙きてれつな形をしています。ちょっと冗長に書き直してみましょうリスト5⁠。

リスト5 関数IFの解説
func IF(
    PRED:()->Bool, // 引数なしでBoolを返す関数
    THEN:()->(),   // 引数なし、戻り値なしの関数
    ELSE:()->()    // 引数なし、戻り値なしの関数
) {
    let dict = [   // [Bool:()->()] な辞書
        true:THEN, // true には THEN を
        false:ELSE // false には ELSE を紐付け
    ]
    let which = dict[PRED()]  // PRED()の結果で辞書引き
    which!()                  // それを実行
}

きてれつではありますが、筋は通ってるのがおわかりいただけたでしょうか。

Swiftが「筋を通しやすい」のは、辞書(dictionary)のリテラルに、Perlによって一般化しJSONによって不朽の地位を得た{}を使っていないこともあります。Swiftでは辞書リテラルは[key:value]の形式をとるので{}が辞書リテラルなのかブロックなのかあいまいになることはありません。{}を見たら例外なくブロック、つまり関数リテラルだと言い切ってしまってよいのです。

ではfuncは不要かというと……

ここまで見てきたとおり、Swiftにおける関数の本質は{}にあり、func(){}という構文糖衣は無用の長物に思えます。しかしこれが必要になるケースが2つほど存在します。

1つは、再帰関数。先ほどの例のfactも再帰的に定義されています。ここでもう一度「ふつう」に書き直してみましょう。

func fact(n:Int)->Int {
    return n <= 1 ? 1 : n * fact(n - 1)
}

これを、こう書き直してみましょう。

let fact:(Int)->Int = { n in
    return n <= 1 ? 1 : n * fact(n - 1)
}

println(fact(10))

一見動きそうですが、こんな感じに怒られてしまいます。

<EXPR>:9:29: error: variable used within its own
initial value
    return n <= 1 ? 1 : n * fact(n - 1)

これを防ぐためには、先にvarつまり変数として宣言だけしておいたうえで、それに定義を代入しなければなりません。

var fact:(Int)->Int
fact = { n in
    return n <= 1 ? 1 : n * fact(n - 1)
}

なんとも冗長であるうえに、これではfactを上書きできてしまいます。残念ながら(?⁠⁠、Swiftには"use strict"されていないJavaScriptのarguments.callee()やR言語のRecall()相当の、自己再帰のための構文はありません。素直にfuncしましょう。どうしてもという方のために、

func recall<T,R>(f:((T->R),T)->R)->T->R {
     var r:(T->R)!
     r = { n in f(r,n) }
     return r
}
let fact = recall { $1 <= 1 ? $1 : $1 * $0($1-1)
}

という手法も一応紹介だけしておきます。何を意味するかは、次回のお楽しみということで。

ただし、ここでfuncが必要になる実例がもう1つ出てきました。総称関数(Generic Functions)です。総称関数とは何か? 型も「変数」になっている関数です。型が変数とはどういうことか? これまた実例で見てみましょう。

func add(x:Int, y:Int)->Int {           // 0
     return x + y
}
func add(x:String, y:String)->String {  // 1
     return x + y
}
println(add(4, 2))      // 6
println(add("4", "2"))  // "42"

まったく同じ名前、まったく同じ定義の関数が2つあります。違いはなんでしょうか? そうです。型です。Swiftでは「関数のフルネーム」は名前と引数の型の組み合わせであり、引数の型に応じて違う関数が呼ばれています。実にありがたい機能ですが、定義が同じなのに型ごとに別の関数を定義しなければならないとなるとずいぶん面倒です。もっとDRY[1]な方法はないでしょうか?

そこで総称関数です。

func add<T>(x:T, y:T)->T {
     return x + y
}
add(4, 2)      // 6
add("4", "2")  // 42

C++のテンプレートやJavaのジェネリクスに相当するこの機能は、Swiftにもしっかり実装されています。add(4, 2)では42からTIntと推論され、結果add(x:Int, y:Int)->Intがコンパイラーによって生成され、add(4,2)ではadd(x:String,y:String)->Intが生成されるというわけです。

しかし、こう書くことはできません。

let add:<T>(x, y)->T = { x, y in
  return x + y
}

なぜそう書けないのか。次回はSwiftにおける総称関数を詳しくみていきます。

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧