Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 673 - Applying MVC 5

講義2の内容は濃いです。Swiftの言語的特徴がちりばめられているからです。

Developing iOS 9 Apps with Swift - Free Course by Stanford on iTunes U

BinaryOperationを実装を考えますが、まずUnaryOperationと同じようにenumのceseのassociated valueを加えます。

    enum Operation {
        case Constant(Double)
        case UnaryOperation((Double) -> Double)
        case BinaryOperation((Double, Double) -> Double)
        case Equals
    }

これを受けてテーブルとしてのDictionaryを次のようにします。

    var operations: Dictionary<String, Operation> = [
        "π": Operation.Constant(M_PI),
        "e": Operation.Constant(M_E),
        "√": Operation.UnaryOperation(sqrt),
        "cos": Operation.UnaryOperation(cos),
        "×": Operation.BinaryOperation(multiply)
    ]

「このmultiplyって関数は何だ?」と疑問に思われるでしょう。実は「multiply」というメソッドは存在しません。これはカスタム関数なのでこれから用意します。

func multiply(op1: Double, op2: Double) -> Double {
    return op1 * op2
}

「class CalculatorBrain {」の上部、要するにクラス宣言外で関数を定義しています。「なぜクラス宣言内にコードしないのか?」という質問が出ました。グローバル関数として扱いたいからというポール先生の答えではありましたが、これはプログラミングスタイルの1つとして考えていいような話になっています。

ということで関数も用意しました。次にこれを利用します...

「じゃあどうする?」ですよな。

考えてみましょう。BinaryOperationの「×」をクリックしたタイミングで、表示されている数字を一時的に保持する必要があります。ボタンを押しても即時に計算される訳ではありません。次に数字(operand)が押されて、「=」キーを押した時に演算が完結させるため、BinaryOperationボタンを押した時は一時的にメソッド自身を保持しなければなりません。

これをどうするべきか工夫が必要です。ポール先生は構造体(Struct)の利用をしました。構造体は異なる型のプロパティを複数持つことができるからです。

    struct PendingBinaryOperationInfo {
        var binaryFunction: (Double, Double) -> Double
        var firstOperand: Double
    }

この型の変数を1つ作ります。でもこれはオプショナル型で生成します。実際にボタンが押されるまで「nil」でいいんです。

private var pending: PendingBinaryOperationInfo?

これを何処で利用するのか?それはperformOperationメソッド内です。

func performOpeation(symbol: String) {
    if let operation = operations[symbol] {
        switch operation {
        case .Constant(let value): accumulator = value
        case .UnaryOperation(let function): accumulator = 
                   function(accumulator)
        case .BinaryOperation(let function): pending = 
      PendingBinaryOperationInfo(binaryFunction: function, 
        firstOperand: accumulator)
        case .Equals: break // まだ実装してません。
       }
    }
}

こういうプログラミング手法が普通なんですね。自分でも考えられるようになりたいです(^_^;) トレーニングを続けたら50オヤジでもできるのだろうか?

これで変数pendingに一時的に数字と関数が保持されました。これを利用するために他の数字(operand)と「=」ボタンが必要です。enum定義時に「.Equals」は作っているのでテーブルとしてのDictionaryを拡張します。

    var operations: Dictionary<String, Operation> = [
        "π": Operation.Constant(M_PI),
        "e": Operation.Constant(M_E),
        "√": Operation.UnaryOperation(sqrt),
        "cos": Operation.UnaryOperation(cos),
        "×": Operation.BinaryOperation(multiply),
        "=": Operation.Equals
    ]

「=」ボタンが押された時の挙動をperformOperationメソッドに加えます。いきなり「=」を押されてもpending変数は存在しない(nil)ので、何も生じないようにします。

 case .Equals:
    if pending != nil {
         accumulator = 
             pending!.binaryFunction(pending!.firstOperand, 
                 accumulator)
         pending = nil
    }

これの説明はいらないですね。コードが明白に内容を語っています。pendingで保持しているoperandと現在表示しているoperandをmultiply関数の引数として渡した計算結果をaccumulatorに代入します。なるほど!

さてランしてシュミレーターで確かめようとしたら「×」も「=」ボタンも無かったのでstoryboardでコピペして拡張します。

f:id:yataiblue:20160511164711j:plain

これでランすると「×」ボタンがちゃんと動きます。しかし、問題が出ます。「5」「×」「3」の後に「=」を押せば「15」と表示されるのですが、「×」を押せば、再び「3」がfirstOperandに保持されて計算結果が表示されません。他のBinaryOperationボタンにも「=」と同様の機能を持たせるために.Equalsの式をprivateメソッドとして抜き出します。

private func executePendingBinaryOperation() {
    if pending != nil {
         accumulator = 
             pending!.binaryFunction(pending!.firstOperand, 
                 accumulator)
         pending = nil
    }
}

これをメソッド内に加えればいいんです。

func performOpeation(symbol: String) {
    if let operation = operations[symbol] {
        switch operation {
        case .Constant(let value):
            accumulator = value
        case .UnaryOperation(let function):
            accumulator = function(accumulator)
        case .BinaryOperation(let function):
            executePendingBinaryOperation()
            pending = 
             PendingBinaryOperationInfo(binaryFunction: function, 
              firstOperand: accumulator)
        case .Equals:
            executePendingBinaryOperation()
        }
    }
}

これで完全に問題が解決された訳ではありません。「=」キーを2回続けて押すと計算がおかしくなります。しかし、ここではそれには触れず、BinaryOperationを拡張していきます。ここからSwift言語で重要な構文の説明に入ります。

テーブルにBinaryOperationを追加していきます。

f:id:yataiblue:20160511172838j:plain

クラス宣言外に「multiply」というカスタム関数を用意しましたが、他のBinaryOperationに対して個別の関数を用意していけばいいのですが、それではコードが煩雑になります。

そこでどうするか?Swiftのパワフルな機能を使います。

クロージャ(Closure)です。クロージャは関数を変数のように扱える特徴があります。既にある「multiply」を使って説明しています。

まずmultiplyの引数を含めたコードをコピーします。

// 以下のコピーを作ります。
(op1: Double, op2: Double) -> Double {
    return op1 * op2
}

これをDictionaryテーブルのmultiplyの引数に置き換えます。

"×": Operation.BinaryOperation((op1: Double, op2: Double) -> Double {
        return op1 * op2
        }),

当然エラーが表示されています。しかし、基本的にこれでいいんです。クロージャーのスタイルに変更してやります。「{」を文頭に持ってきて、「{」があった場所に「in」を加えるとクロージャーのフォームになります。

"×": Operation.BinaryOperation({(op1: Double, op2: Double) -> Double in
        return op1 * op2
        }),

ここからSwiftの持つ強力な推測機能が使えるところがクロージャの真骨頂です。まず「型」の「(Double, Double) -> Double」は明白なので省略できます。

"×": Operation.BinaryOperation({(op1, op2) in
        return op1 * op2

引数があれば、デフォルトで「$」つきのナンバリング(0からスタート)が使えるので、「in」を含めて省略できます。

"×": Operation.BinaryOperation({ return $0 * $1 }),

当然、returnも省略できます。ここで学生から笑い声が上がりました。

"×": Operation.BinaryOperation({ $0 * $1 }),

これがクロージャーのパワーです。クラス内に書いたmultiply関数は消してしまいます。そして同じように他のBinaryOperationを定義します。UnaryOperationも1つ追加します。

    private var operations: Dictionary<String, Operation> = [
        "π": Operation.Constant(M_PI),
        "e": Operation.Constant(M_E),
        "±": Operation.UnaryOperation({ -$0 }),
        "√": Operation.UnaryOperation(sqrt),
        "cos": Operation.UnaryOperation(cos),
        "×": Operation.BinaryOperation({ $0 * $1 }),
        "÷": Operation.BinaryOperation({ $0 / $1 }),
        "+": Operation.BinaryOperation({ $0 + $1 }),
        "−": Operation.BinaryOperation({ $0 - $1 }),
        "=": Operation.Equals
    ]

これで計算機の基本的な部分*1を説明したので次はUIの話に入ります。iPhoneのポートレイトとランドスケープに対応するためにAutolayoutの説明が入ります。

長くなったので今日はここまで。

*1:オペレーションボタンを2度押しした時の不審な挙動は放置されています。あくまでもデモなので語れていません。「.」の実装は宿題のようです。私もそのうち取り組んでみます。