2014年8月21日木曜日

[Swift] 関数引数としてのタプル

以前書いた「カリー化関数の話」という記事の後半で
言われてみれば確かに「関数の引数定義」は「タプルの名前付き定義」の書き方と同じ。
() は、もしかして「結合順序」と言うより、タプルの境界を定義するものと考えた方が良いのかも、しれない。
などと書いていたのだが、ようやくこれを試してみた(Xcode6-Beta6)。

例えば以下のような関数を定義したとする。
func f(a: Int, b: Int) {
    println("a + b = \(a + b)")
}
通常は次のように使う。
f(1, 2)
こうなると (1, 2) はタプルに見えてくるだろうと言うことで、タプルそのものを渡せるのか。結論から言うと渡すことが出来た。
let v = (1, 2)
f(v)
これはかなりナイス。

名前付き引数を要求されている場合なら、名前付きタプルで OK。
func f(#a: Int, #b: Int) {
    println("#a + #b = \(a + b)")
}

let  v = (a: 1, b: 2)
f(v)
あちこちで使うようなテクニックでは無いにしろ、使えるのは便利だし、関数の引数定義はタプルを拡張したものであるようなことが伺える。

しかしいくつかの制限も見られた。


まず、引数に渡せるタプルは let 宣言に限られていて、var 宣言のタプルはエラーになった。
しかしこれはキャストすれば回避可能であった。
func f1(a: Int, b: Int) {
    println("a + b = \(a + b)")
}
var v1 = (1, 2)
// f(v1)            // error: Missing argument for parameter #2 in call
f(v1 as (Int, Int)) // success
名前付き引数なら、当然名前付きタプルでキャストすれば良い。
func f2(#a: Int, #b: Int) {
    println("#a + #b = \(a + b)")
}
var v2 = (a: 1, b: 2)
// f(v2)                  // error: Missing argument for parameter 'b' in call
f(v2 as (a: Int, b: Int)) // success

次に、引数が1つの場合は名前付きタプルは文法上渡せなかった。
func f3(#a: Int) {
    println("#a = \(a)")    
}
let  v3 = (a: 1)
// f3(v3)             // error: Missing argument label 'a:' in call
// f3(v3 as (a: Int)) // error: Cannot create a single-element tuple with an element label

let  v4: (a: Int) = (a: 1) // error: Cannot create a single-element tuple with an element label
Beta6 からは単一要素のタプルには名前がつけられなくなっている。
上記の例では v3.a にはアクセスできない。

元々、(Int) という宣言は、単なる Int 型として見られていたので、これが Beta6 でより強化されたのだろう*1

名前付き引数でなければ問題はないし、単一要素をわざわざタプル化しないだろうから現実的に問題ではないと言って良い。


「関数引数 ≒ タプル」と見た場合、総合的にはまぁ想定の範囲内の結果。
気になるのは var タプルの場合のキャストくらい。しかしこれも正式版までにはまた変わりそうな気もする。

...
*1: 余談だが Beta5 までなら上記の f3(v3 as (a: Int)) と f3(v4) は成功を確認している。

2014年8月7日木曜日

[Swift] コマンドライン(Command Line)プログラムとして使う [2] 外部モジュールの作成と使用

前回の続き。
思ってたより全然面倒な話だったので、試行過程も記録しておく。


さて。なぜ使い回すソースコードをモジュール化するかというと、Objective-C と違い Swift の import はヘッダファイルではなくモジュールだから、ということになるだろう。

モジュールを作る


とりあえず作ってみる。
xcrun swift -help を眺めると、モジュール化できそうなオプションがいくつか見つかる。

まずはモジュール用のサブディレクトリを作り、モジュール用のソースコードを以下のように書いた。
/* modules/Example.swift: モジュールとして使われる方 */
public func MyFunction(name: String) -> String {
    return "Hello, \(name)"
}

public class Example {
    public let  name: String

    public init(name n: String) {
        name = n
    }
}
外から参照できるようにすべて public にした。そして以下のモジュール化コマンドを実行。
% /Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift \
    -frontend \
    -emit-module \
    -module-name Example \
    -emit-module-path modules/Example.swiftmodule \
    -emit-module-doc-path modules/Example.swiftdoc \
    modules/Example.swift
modules/ に Example.swiftmodule と Example.swiftdoc が作成される。

次はモジュールを使う側のファイルを(前回からの続きの command-6.swift として)作成。
#!/Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift -I ./modules
import Example

println(MyFunction("Module"))

var  module = Example(name: "Command Line")
println("Hello, \(module.name)")
-I オプションでモジュールの場所を伝える。

しかし、これを実行するとエラーとなる。
% ./command-6.swift
LLVM ERROR: Program used external function '_TF7Example10MyFunctionFSSSS' which could not be resolved!
import は成功してるけど、シンボルを解決できていない。

swiftmodule の中には実行データが入ってないのか。思えば Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx には .swiftmodule、.swiftdoc の他 .dylib も一緒に入ってる。ということで dylib が必要なのだろう。


ライブラリを作る


ライブラリ化するため、今度はコンパイラ swiftc を使う。まずはオブジェクトファイルを作成。
% /Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swiftc \
    -emit-library \
    -emit-object modules/Example.swift \
    -o modules/Example.o
出来たオブジェクトファイル(modules/Example.o)から dylib を作成する。
% /Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun libtool \
    -macosx_version_min 10.9 \
    -L/Applications/Xcode6-Beta5.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
    -dynamic \
    -install_name @rpath/libExample.dylib \
    -lswiftCore \
    -lSystem \
    -o modules/libExample.dylib \
    modules/Example.o
これでライブラリが modules/libExample.dylib として作成された。

install_name オプションで @rpath を指定したのは executable 側が検索パスを決めるため。今回のケースでは dylib を別の場所で使うワケではないので影響しないが、.app に入れることも考えればあった方が良いオプション。

作成した dylib を使用するように先の command-6.swift を修正する。
#!/Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift -I ./modules -L ./modules -lExample
import Example

println(MyFunction("Module"))

var  module = Example(name: "Command Line")
println("Hello, \(module.name)")
-L オプションでライブラリの場所を、-l でリンクするライブラリを指定した。

これで実行できるかと思いきや、またもやエラーが。
% ./command-6.swift
<unknown>:0: error: could not load shared library 'libExample'
ライブラリを見つけられていない。

これに小一時間ハマってしまったがなんのことはない、実行時カレントディレクトリに libExample.dylib があれば見つけてくれた。
-L オプションはリンク時のライブラリの場所であって、実行時のライブラリの場所ではないってことだ。

せっかくスクリプトっぽく使えると思ったのに(いちいちライブラリ化するのは我慢するとしても)ライブラリと同じディレクトリにいないと実行できないとかよろしくない。

実行時ライブラリ検索は LD_RUNPATH_SEARCH_PATHS だろうから、これを指定する方法を模索したのだが、残念ながらこれも結局 swiftc でビルドするしか無い、という結論に落ち着いた。
% sudo xcode-select -switch /Applications/Xcode6-Beta5.app
% xcrun swiftc -I ./modules -L ./modules -lExample -Xlinker -rpath -Xlinker ./modules -o ./command-6 ./command-6.swift

% ./command-6
Hello, Module
Hello, Command Line
実行成功。いやビルドしちゃってるから。当たり前すぎて感動がない、っていう。

リンカオプションとして rpath を指定するのだけど、この時ばかりは xcode-select -switch しないとエラーになってしまった。

中で使われているであろう xcodebuild がなんか解決できなくてエラーになっていたようなので、ビルドするのなら素直に -switch しときましょう、と。


まとめ


スクリプトのように書けると思われた Swift コマンドラインプログラムだがひとたび自作の外部モジュールを使うとなると、モジュールのライブラリ化が必要で、使う方は executable としてビルドするか、もしくはライブラリをカレントにコピーしないといけない。

そういうものかと思う一方で、ここら辺に関しては本来 xcrun swift に良きに計らってもらいたいという気持ちもある。

Xcode を使うことでこれらの手間を軽減できるかと思いきや、現状(Xcode6-Beta5)は Command Line Tool もモジュール関係もテンプレートがない。

今のところコマンドラインは、そう言う手段がある、という程度が無難かも。
正式版に期待。


参考



2014年8月6日水曜日

[Swift] コマンドライン(Command Line)プログラムとして使う [1]

そう言えばコマンドラインとして使える話があったので、今更試してみたメモ。

コマンドラインツール群はコンパイラ等と同様にアプリケーションパッケージ内にある。
通常は xcode-select -print-path で表示されている Xcode 内のツールが使われるが、現状ベータなので xcode-select -switch で変更せずに直接パスを叩く方針にする。

REPL(Read-Eval-Print-Loop)を使ってみる


まずは REPL を立ち上げてみる。
% /Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift
Welcome to Swift!  Type :help for assistance.
  1>  
または以下でも同じものが立ち上がる。
% /Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/lldb --repl
% /Applications/Xcode6-Beta5.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift
終了するには Ctrl-D を押すか、REPL に :q (または :quit) とタイプする。

一通りのことは出来るようだが、出来ることに若干の違いがあるような気がする。


ファイルを実行する


コマンドラインなので、REPL よりもファイルから実行できた方が良い。
以下のようなフツーの swift ファイルを書いて実行することが出来た。
func hello(name: String) -> String {
    return "Hello, \(name)"
}

println(hello("Swift"))
% /Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift command-1.swift
Hello, Swift
古いサンプルでは xcrun swift -i の指定があるものも残っているが、Beta5 から -i オプションは必要なくなっている。

通常のアプリケーションコードと違うのは、コマンドラインや REPL の場合は Playground と同様に order-dependent なので定義順にしか解釈されないこと。
アプリケーションコードであっても main.swift のようにトップレベルに実行可能コードが書けるものは order-dependent となるのだが、この辺の話は Apple Swift Blog の Files and Initialization に書いてある。

当たり前だが REPL や Playground とは違い、コマンドラインでは標準出力に出力しなければ結果は表示できない。


よりコマンドラインらしく、shebang を書いて実行権限をつければ単体で実行可能となる。
#!/Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift

func hello(name: String) -> String {
    return "Hello, \(name)"
}

println(hello("Swift"))
% chmod +x command-1.swift
% ./command-1.swift
Hello, Swift
まるでスクリプトのように実行できる。


引数を取る


さて、コマンドラインとして使うなら引数が取れないと意味が無い。

Foundation を使えば Cocoa の流儀で取得できる。
#!/Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift
import Foundation

let  keyName = "keyName"
let  args = NSUserDefaults.standardUserDefaults()

println("\(keyName): \(args.objectForKey(keyName))")
% ./command-2.swift -keyName value
keyName: value

% ./command-2.swift -keyName '("a", "b")'
keyName: (
    a,
    b
)

% ./command-2.swift -keyName '{ "k1"=1; "k2"=2; }'
keyName: {
    k1 = 1;
    k2 = 2;
}
Array や Dictionary の指定が Swift-Style ではなく ASCII Property List Style になってしまうのは NSUserDefaults の都合上、致し方ない。

特定の引数を取得するのは NSUserDefaults で良いが、引数一覧を取得する場合は NSProcessInfo を使う。
#!/Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift
import Foundation

for arg in NSProcessInfo.processInfo().arguments {
    println("\(arg)")
}
% ./command-3.swift a b c
/Applications/Xcode6-Beta5.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift
-frontend
-interpret
./command-3.swift
-enable-objc-attr-requires-objc-module
-target
x86_64-apple-darwin13.3.0
-target-cpu
core2
-module-name
main
-sdk
/Applications/Xcode6-Beta5.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk
-color-diagnostics
--
a
b
c
実はたくさんの引数がついていた。

Foundation を使わずに Pure Swift で引数を取ることが出来ないか調べたところ、practicalswift.com がヒットした。どうやら Process というのが定義されているらしい。
#!/Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift

for arg in Process.arguments {
    println("\(arg)")
}
% ./command-4.swift a b c
./command-4.swift
a
b
c
これなら Pure Swift だし、NSProcessInfo を使うよりも実用的な引数一覧になった。

しかし、Process の実態には Xcode 上で定義にジャンプすることが出来ず、どこにどのように定義されているものかわからない。

この定義をどうやって見つけたのだろう?


他のモジュールを使う


先の例までで Foundation を使っていたが、当然 AppKit も使えるだろうということで、GUI テストとしてアラートを表示してみる。
#!/Applications/Xcode6-Beta5.app/Contents/Developer/usr/bin/xcrun swift
import Cocoa

var  alert = NSAlert()
alert.messageText = "Hello, Swift"
alert.runModal()
% ./command-5.swift
問題なく表示できた。これは素晴らしい。


これなら GUI が必要な時はシェルスクリプト代わりに swift を使うのもアリだ。


では自作のモジュールを取り込むときは?
シェルスクリプトで使い回し可能コードを source して取り込むように、swift コマンドラインで使い回し可能コードを import で取り込めないものか。

これを行う為には、使い回す swift コードをモジュール化する必要がある。


続きは次回