読者です 読者をやめる 読者になる 読者になる

WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

SwiftでCUIプログラミングしてみた

Swift

今回エンジニアブログを担当する、サーバーチームの原です。

最近巷を騒がしているApple発のSwift。
私はサーバーチームなのでiOSアプリを実装する予定は今のところ無いのですが、
新しい言語と聞いては試さずにはいられません。

とりあえず、Xcodeを使わずCUIベースで、ものすごく簡単なCLIプログラムを書いてみました。
全体が見やすい規模ですので、言語仕様を読みきれないという方も、少しは雰囲気を味わっていただけるかと。

なお、私が今回試した環境はXcode 6 beta 1です。beta版がゆえに、将来的に変更されうる箇所があることをご了承ください。

事前準備

Swiftをターミナル上でビルドするためには、Xcode 6 betaインストール後にもう少し事前準備が必要です。

まず、Xcode 6 betaに付属されているswiftコマンドにパスを通します。

$ SWIFT_CMD=`find /Applications/Xcode6-Beta.app -name swift | grep /bin/swift`  
$ XCODE_BIN=`dirname $SWIFT_CMD`  
$ echo "export PATH='$PATH:$XCODE_BIN'" >> ~/.bash_profile  
$ source ~/.bash_profile  

これで、swiftコマンドが実行できるようになりました。

次にOS X SDKへのパスをシェル変数に入れておきます。
これは、FoundationやCocoaなどのフレームワークをimportするためのコマンドオプション指定をしやすくするためです。

$ OSX_SDK=$(xcrun --show-sdk-path --sdk macosx)  

簡単な電卓プログラムを実装してみる

標準ライブラリへの依存もなるべく抑え、Swiftを触ったことないひとでもコードが読める程度のプログラムにしてみました。
クラスや構造体、プロトコルなどは出てきませんが、いくつかのSwiftの特徴は表れていると思います。 Syntax Highlightingが微妙なのはお察しください・・・

import Foundation  
  
let operators: Dictionary<String, (Int, Int) -> Int> = [  
    "+": {$0 + $1},  
    "-": {$0 - $1},  
    "*": {$0 * $1},  
    "/": {$0 / $1},  
]  
  
enum Result {  
    case Success(Int)  
    case Error(String)  
}  
  
func calc(op1: Int?, opr: String, op2: Int?) -> Result {  
    if op1 && op2 {  
        let n1 = op1!  
        let n2 = op2!  
  
        if let fn = operators[opr] {  
            let answer = fn(n1, n2)  
            return Result.Success(answer)  
        } else {  
            return Result.Error("Unsupported operator: \(opr)")  
        }  
    } else {  
        return Result.Error("Operands must be numeric.")  
    }  
}  
  
// main  
  
var fh: AnyObject! = NSFileHandle.fileHandleWithStandardInput()  
while true {  
    if let data = fh.availableData() {  
        let str = NSString(data: data, encoding: NSUTF8StringEncoding)  
        let charset = NSCharacterSet.newlineCharacterSet() as NSCharacterSet  
        let line = str.stringByTrimmingCharactersInSet(charset)  
        let expr = line.componentsSeparatedByString(" ")  
  
        if expr.count != 3 {  
            println("USAGE: {num} (+|-|*|/) {num}")  
            continue  
        }  
  
        let op1 = expr[0].toInt()  
        let opr = expr[1]  
        let op2 = expr[2].toInt()  
  
        let result = calc(op1, opr, op2)  
  
        switch result {  
        case let .Success(answer):  
            println("ANSWER: " + String(answer))  
        case let .Error(msg):  
            println("ERROR: " + msg)  
        }  
    }  
}  

上記のプログラムをCalc.swiftというファイル名で任意のディレクトリに保存し、以下のコマンドを実行しましょう。 ターミナル上で四則演算のみをサポートする簡単な電卓プログラムが起動します。

$ swift -sdk $OSX_SDK -i ./Calc.swift  
1 - 1  
ANSWER: 4  
9 / 3  
ANSWER: 3  
x - 1  
ERROR: Operands must be numeric.  
3 ^ 2  
ERROR: Unsupported operator: ^  

解説と所感

それでは、上記のプログラムでのSwiftの特徴をみてみましょう。

クロージャと型推論

let operators: Dictionary<String, (Int, Int) -> Int> = [  
    "+": {$0 + $1},  
    "-": {$0 - $1},  
    "*": {$0 * $1},  
    "/": {$0 / $1},  
]  

文字列のオペレータと実際に四則演算をする関数をマッピングする辞書を定義しています。
Swiftのクロージャはいくつか記法があるのですが、上記のコードはそのうち最も簡略化したものです。

引数の型はoperatorsの型注釈において自明であるため、型推論が働き、省略可能となります。
また、Swiftのクロージャは仮引数を省略すると、$nという引数が暗黙的に定義されます。

Dictionaryの型パラメータが若干うざい感じもしますが、各クロージャに型を指定するよりは、断然見やすいでしょう。

関数の型の記法も、Haskellっぽい記法で気に入ってます。 型推論もしっかり効いてるので、より複雑なプログラムでも信頼性と可読性に期待が持てそうです。

代数的データ型

enum Result {  
    case Success(Int)  
    case Error(String)  
}  

Resultという列挙型にSuccessとErrorそれぞれにフィールドが定義されています。 このIntとStringは列挙型の各メンバーのraw valueではなくAssociated values、とのこと。
ちなみに複数のフィールドを持たせることができます。

これらの列挙型を使うには、以下のようにインスタンスを生成します。

return Result.Error("Operands must be numeric.")  

この列挙型インスタンスからAssociated valuesを取得するにはswitchによるパターンマッチを使用します。

switch result {  
case let .Success(answer):  
    println("ANSWER: " + String(answer))  
case let .Error(msg):  
    println("ERROR: " + msg)  
}  

思えば、C言語のenumからすいぶん進化したものです・・・
Haxeを実際には触ったことないですが、機能的にはHaxeのenumっぽいです。

switch文ではちゃんと網羅性もチェックしてくれているので、非常に便利。

Optional value

func calc(op1: Int?, opr: String, op2: Int?) -> Result {  
    (略)  
}  

Int?は、IntをwrapしたOptional valueを表す型で、値が存在しない(nilである)可能性を示す型です。
ifの評価式にOptional valueを使うと、値が存在する場合に真に評価されます。

    if op1 && op2 {  
        let n1 = op1!  
        let n2 = op2!  
        (略)  
    } else {  
        return Result.Error("Operands must be numeric.")  
    }  

Optional valueそのままをIntのように使うことはできませんので、
「!」演算子を使うことでOptional valueから中の値を無理やり取得しています。
nilの場合は実行時エラーが発生するので注意が必要です。

上記のコードではnilで無いことが自明ですので使っても特に問題はないとは思いますが、
本来はなるべく避けるべきものです。

本当は下記のように書きたかったのでしたが、コンパイルエラーになってしまいました。

if let n1 = op1 && let n2 = op2 {  

if let n1,n2 = op1,op2 {  

など。

if文一発で、複数のOptional valueをunwrapする方法が欲しいですね・・・
ちなみにif letの動作をカスタマイズすることはできないので、Monad用の構文としては使えなさそう。残念。

なお、辞書からの値取得にもif letが使えます。

if let fn = operators[opr] {  
    let answer = fn(n1, n2)  
    return Result.Success(answer)  
} else {  
    return Result.Error("Unsupported operator: \(opr)")  
}  

Objective-Cとの互換性

var fh: AnyObject! = NSFileHandle.fileHandleWithStandardInput()  
while true {  
    if let data = fh.availableData() {  
        let str = NSString(data: data, encoding: NSUTF8StringEncoding)  
        let charset = NSCharacterSet.newlineCharacterSet() as NSCharacterSet  
        let line = str.stringByTrimmingCharactersInSet(charset)  
        let expr = line.componentsSeparatedByString(" ")  

Foundationフレームワークのクラスを使用して、標準入力で入力された値を取得しています。

Objective-Cを実際にプログラミングしたことは無いのですが、
これらのクラスはSwift登場以前からObjective-Cから使用できるAPIでした。
このように、Objective-C用に設計されたクラスをそのままSwiftから使うことができます。

ただ、個人的にこのAPIに全然慣れていないので、メソッド名の長さが非常に気になります・・・
とくに自分は命名をシンプルにしようとする傾向があるので、このコード片だけ雰囲気が違ってます。

また、NSCharacterSet.newlineCharacterSetの戻り値をキャストする必要性が何故あるのか、APIドキュメントを見てもよく分かりませんでした。
なぜAnyObject?が返されるのか・・・
ここらへんがObjective-CとSwift間での相互運用上の妥協点なのでしょうか。

また、当初は以下のようなコードを書いてバグを出ていました。

        let str = NSString(data: data, encoding: NSUTF8StringEncoding)  
        let expr = str.componentsSeparatedByString(" ")  

このコードだと、標準入力された2つ目のオペランドに改行コードが含まれてしまいます。 その影響かはわかりませんが、exprがAnyObject?[]型の値になってしまい、
数値として扱うためには、一度Stirngにdowncastしなければなりませんでした。

str.componentsSeparatedByStringが処理結果によって戻り値の型を変えるという振る舞いは かなり危険ですので、特段の注意が必要です。1

まとめ

簡単なプログラムを通じて、Swiftの特徴のほんの一部を見てみました。

型注釈の省略と型推論、クロージャや代数的データ型など、モダンなプログラミング言語にふさわしい機能をもっていますが、
Objective-Cとの相互運用性は確保されていますが、既存APIへの利用、特に型に関しては、注意して記述する必要がありそうです。


  1. 自分がAPIの仕様を理解していないだけかもしれません。詳細をお知りの方はご指摘いただけると助かります