sci

最果て風呂

mto を Objective-C で実装してみる

前口上

長いこと取り組もうと思っていた Objective-CCocoa による mto の実装。少し調べては放置ということをずっと繰り返していて、なかなか成果品を生み出すことができないでいた。

この暮れの休みには不幸にして体調を崩してしまい寝正月になってしまったのだけれど、寝る以外には他にやる事が無く、幸いなことに CUI 版だけれども Objective-C による実装をすることが出来てしまった。はじめてみれば取っ付きにくい部分はあるものの、短期間(2日間)で完成することが出来たのだった。

Cocoa で作る前には C# .NET Framework による Windows 用の GUI 版を作ったり、Go 言語による実装なんかもしていた。本命があるにもかかわらず、後回しにする癖をなんとかしたいものだ(^_^;)

作業については日記には書いているものの、日付でバラバラになってしまっているので、参照しやすいようにこのページにまとめておくことにした。うわ〜、もう 2 年も前のことだよ。すっかり忘れてる……

下積み

2012-04-08

8 年前の PDF だけれど、BecomeAnXcoder を読みはじめた。C の入門編を読んで malloc や free が出てきて少し高度な C を知ったので、その延長線上として Xcode でプログラムをしてみたくなったわけ。

今は明確な目的があるのでかなり真剣に読める。まぁ、mto の Mac 版を作ろうってことなんだけどね。文字列の扱いがかなり面倒なのでどうなることやら。

2012-04-09

NSString は変更できない。NSMutableString は変更できる。immutable というのは Python でタプルを使った時にはじめて知ったのだった。もう Python は使ってないけど触っておいて良かった。

NSString で宣言する場合は * を付ける。ポインタだけど難しく考えることはなく、文字列を使う時は * を付けるとだけ覚えておけば良い。また、オブジェクトであることを強調(という訳ではないけど)するために文字列の前に @ を付ける。そんなものだと思っておけば良い。

メソッドの適用方法がおもしろい。ドットで繋げるやり方に慣れているので困惑気味。「オブジェクトに対してメッセージを送信する」という表現がなされているので、メソッドの適用と言っては間違いなのかもしれない。

NSString *homu;
homu = @"ほむほむ";
nagasa = [homu length];
NSLog(@"%d", nagasa);

4 を期待していたけど 12 になっちゃった。unicode が 3 バイトだから?ASCII とマルチバイトをどうやって判別すれば良いのかな?あと、4 バイトで表現される漢字の旧字体もあるから単純に 3 を掛けるという方法は使えないね。実行ウインドウの日本語が文字化けするのはフォント設定がおかしいから?

NSMutableString で初期化する時は下記のように mutableCopy という余計なものを付けないといけないらしい。面倒。

NSMutableString *mami
mami = [@"まみまみ" mutableCopy];

日本語が出ないよぉ。うわ、文字列の説明短け〜。

配列、ちょっとわからん。何でブラケットで囲われてるんだろう。NSMutableArray が何度も出てきてかなわないなぁ。

NSMutableArray *hogeArray = [NSMutableArray array];

配列にはどんなオブジェクトも格納できるらしいけど、int を入れようとしたら出来なかった。addObject ではダメってことかしら。ハマっている人が多いらしく、日本語で検索するとすぐに引っ掛った(英語だと StackOverflow ばかり引っ掛かる)。

NSMutableArray *hogeArray = [NSMutableArray array];
NSString *mami = @"mamimami";
NSString *tiro = @"finale";
int fuga = 5;
[hogeArray addObject:mami];
[hogeArray addObject:tiro];
[hogeArray addObject:[NSNumber numberWithInt:fuga]];
NSString *element = [hogeArray objectAtIndex:1];
NSLog(@"%@", element);

というように、[NSNumber numberWithInt:xxx] を使って変換(というかオブジェクト化)してやるのだとか。

2012-04-10

昨日の続きでリファレンスの見方。

addObject:
- (void)addObject:(id)anObject
 Adds anObject to the receiver if it isn’t already a member. If anObject is already a member, addObject: increments the count associated with the object. In either case, anObject is then sent a retain message.

先頭が - のものはインスタンスメソッドvoid なので返り値は無い(送りっぱなし)。セットする型は id (オブジェクト)なで、値はオブジェクト(anObject)でないといけない。ちなみに + はクラスメソッド

ということなので、昨日の [NSNumber numberWithInt:100] がやっていることは、100 という値を持つ NSNumber オブジェクトを作成しているということになる。オブジェクトの名前を付けなくても使えるんだね。

numberWithInt:
+ (NSNumber *)numberWithInt:(int)value
 Creates and returns an NSNumber containing value, treating it as a signed int.

(NSNumber *) の意味って何だろう?アスタリスクが付くってことは番地を返すってことなの?ドット構文も使えるそうな。下記はどちらも同じ意味になるとのこと。

[foo setValue:100];
foo.value = 100;

やっぱりドットを使った方がわかりやすいなぁ。単にブラケット記法に慣れていないだけという話もある。日曜スクリプターはどっちを使っているのだろう?ソースが公開されているプロジェクトを見たことがない。

C# のソースもそうなのだけど、IDE で作成したものって公開するのにどのファイルが必要なのかわからない。Visual StudioForm.cs だけではビルドできないし、Xcodemain.m だけではビルドできないし。

プロパティ関係もろもろが必要となると、プロジェクトディレクトリ以下全てが必要になってしまう。そうすると username とか個人的なディレクトリ構成とかがバレちゃうよね?だから Win 系や Mac 系でソースを公開している人が少ないのかな。単に自分が知らないだけとも言えるけど。

NSArray       リスト
NSDictionary  辞書
NSSet         集合
NSCountedSet  集合(重複なし)

2012-04-11

CocoaFundamentals を読んでいるのだけど、日本語訳なのに内容を理解することが出来ない。横文字が多過ぎて用語がわからんのだ。ブラケット記法には目が慣れてき感じ。

2012-04-19

C#GUI アプリを簡単に作れた事に味を占めて二週間前から古い Xcode(1.1) で GUI App 作成の練習をしているけど、Objective-C の部分が難しい。しばらくは CUI でごにょごにょする日々が続きそう。MBA を購入できたら勉強し直すことになるけど、いつの日になることやら。

しばらくは Command Line Tool を作りながらいろいろと勉強していくことにする。優先的に知るべきものは下記になるのではないかと思う。ポインタは分っているつもりだけど怪しいレベル。

  • ファイルの読み込み・書き込み(辞書ファイルから読み込む)
  • 二次元配列の作成(連想リスト的なもの。Rubyのように順序を保つならhashでも良い)
  • 文字列の検索・置換(文字列の置き換え)
  • foreach 等による繰り返し(やっぱり while を使うのかな)
  • スコープ(辞書の寿命。現状ではボタンを押す度に作り直したい)

CUI 版を作る

2014-01-04

○ファイルの読み込み

読み込むファイルのパスは決め打ちになってしまうけれど、UTF-8 のファイルを読み込んで標準出力に表示するには下記のようにすれば出来る。

型にパス形式があったような気がするけれど、NSString 型にしても上手くいくようなのでこれを使った。

error:nil の部分については Go 言語と同じような使い方ができるそうだけれど、今の段階では nil にしておく。なお、ファイルが開けなかった等のエラーが生じた場合は text は空なので null が表示される。

#include <Foundation/Foundation.h>

int main(int argc, const char * argv[])
{
    NSString *filePath = @"/Users/home/kana-jisyo";
    NSString *text = [NSString stringWithContentsOfFile:filePath
                                               encoding:NSUTF8StringEncoding
                                                  error:nil];
    NSLog(@"%@", text);
}

○二次元配列の作成

C# や Go 言語と同じように正規表現オブジェクトをコンパイル(って言うのかな?)してから適用することになる。正規表現はもっと簡単に 1 回で処理できると思うけど、それは今後の課題ということで。

ファイルを読み込んで一行ずつ処理するというメソッドが無いので、一度全部読み込んでしまい、それを改行文字(\n)で区切って文字列の配列を作るという方法にする。下記の例では NSArray *lines = ... の部分。これは Ruby でいうところの .readlines をした状態になる。

配列に対する繰り返しは for ... in ... でやる。if 文のところをもっとスマートにしたいけどとりあえずはやりたいことを羅列して欲しい結果を得ることにした。

NSTextCheckingResult.numberOfRanges ではヒットすると 1 を返し、そうでなければ 0 を返すとのこと。これを利用して if 文で分岐させている。今回の場合はコメント行(行頭 ;... や 空行)だった場合は無視して、そうでなければ次の処理をするというもの。

次の処理にまわされたものは、上と同じことだけれどコメント文(car /cdr ;... の空白+;)を除去する処理をする。これは stringByReplacingMatchesInString: ... withTemplate: ... を使う。

さらに続けて componentsSeparatedByString:@" /" を使って「car /cdr」となっている文字列を " /" で区切って [car, cdr] という配列にし、addObject で dict1 配列に追加していく。これで二次元配列が出来るという訳。

.....
// 辞書ファイルのコメント行を削除するための正規表現
NSString *pat1 = @"^;.*|^$";

// 辞書ファイルのコメント部分を削除するための正規表現
NSString *pat2from = @"\\s+;.*";

// コメント部分を削除(空)にするためのもの
NSString *pat2to = @"";

// 内部変換辞書を作る
NSMutableArray *dict1 = [NSMutableArray array];
        
// 改行文字で区切って配列に格納する
NSArray *lines = [text componentsSeparatedByString:@"\n"];
        
NSRegularExpression *regexp1 =
[NSRegularExpression regularExpressionWithPattern:pat1
                                          options:0
                                            error:nil];
        
for (NSString *line in lines) {
    NSTextCheckingResult *match =
    [regexp1 firstMatchInString:line
                        options:0
                          range:NSMakeRange(0, line.length)];
            
    if(match.numberOfRanges){
        // NSLog(@"%@", match);
    }
    else {
                
        NSRegularExpression *regexp2 =
        [NSRegularExpression regularExpressionWithPattern:pat2from
                                                  options:0
                                                    error:nil];
        NSString *result =
        [regexp2 stringByReplacingMatchesInString:line
                                          options:0
                                            range:NSMakeRange(0,line.length)
                                     withTemplate:pat2to];
              
        [dict1 addObject:[result componentsSeparatedByString:@" /"]];
.....

○文字列の検索・置換

まずは単純に文字列の検索・置換をやってみる。データはソースに決め打ち。検索置換をするにはstringByReplacingOccurrencesOfString:key withString:value を使うとのこと(key, value はそれぞれ検索文字、置換文字でハッシュのアレを意味しているのはない)。

#include <Foundation/Foundation.h>

int main(int argc, const char * argv[])
{
    NSString *str = @"笑う門には福来る。ご冗談でしょう?笑う。";
    NSString *strCar = @"笑う";
    NSString *strCdr = @"笑ふ";
    str = [str stringByReplacingOccurrencesOfString:strCar
                                         withString:strCdr];
    NSLog(@"%@", str);
        
    return 0;
}

これを実行すると下記の結果を得られる。

笑ふ門には福来る。ご冗談でしょう?笑ふ。

○foreach 等による繰り返し

for ... in ... で実現できる。

○外部ファイルを読み込んで検索・置換したものを表示する

今まで出てきたものを使って実現が出来る。検索置換は stringByReplacingOccurrencesOfString ... withString ... を使う。

.....
NSString *inFilePath = @"/Users/home/momotaro.txt";
NSString *inText = [NSString stringWithContentsOfFile:inFilePath
                                             encoding:NSUTF8StringEncoding
                                                error:nil];
for(id obj in dict1){
inText = [inText stringByReplacingOccurrencesOfString:obj[0]
                                           withString:obj[1]];
}
NSLog(@"%@", inText);
.....

○スコープについて

よくわからん。

○まとめ

mto の Objective-C での実装。とりあえずやりたい機能は実装することが出来た。各ファイルへの情報はソースに決め打ちの状態で引数処理はこれから。関数の作り方というかクラスの設計について良くわからない。今までのように 1 ファイルでは出来そうもなくて、.h.m を使わないといけないみたい。

話しは変わるけど Xcode って使いやすい。でもこれ、Cocoa Emacs 24.3 と同じように、日本語が入ると行間がおかしく(ちょい広く)なる。なんなんだよぉ(>_<)、常用するには厳しいかもしれない。SKK で入力できないから日本語入力が辛いわ。と思ったけど、ことえりでも十分だわ。癖で Shift キーを押しちゃうからぐぬぬになるけど。コメントも英語で書くようにすればいいのかなぁ。というか何をしているのかわかるような関数名を付けなきゃいけないから、コメントは不要なのかなぁ。

自分のやりたいことは 1 ファイルで実現できるんだけど、いちいち interface で宣言して implementation で実装しなきゃいけないというのは辛い。この作法に慣れるまでは面倒だし苦しいわ。

そもそもオブジェクト志向ってのが良くわからない。自分のやりたいことを実現するためには「内部辞書というデータがあって、これに検索置換というメソッドを持たせたクラス」を設計しろってことなのかなぁ。そしてインスタンスを作る時に任意の辞書データを作り、読み込んだファイルにこれを適用させると結果が出てくるという方針かな?

.h と .m に分ける

2014-01-06

main.m.h.m に分けて期待する動作をするまでになった。クラス変数は .h でなく .m に書いておくこともできるらしい。また、@property を使うといろいろと便利らしいのでそうしておいた。

○interface について

interface には変数を書かず、メソッドのプロトタイプだけを記述するのが最近の流儀らしい。

MTODict.h

@interface MTODict : NSObject

- (id)initWithDict:(NSString *)dict;
- (void)replaceStringCar:(NSString *)file;
- (void)replaceStringCdr:(NSString *)file;
- (void)printDict;

@end

MTODict.m

#include <Foundation/Foundation.h>
#import "MTODict.h"

@interface MTODict()

@property NSMutableArray *innerDict;

@end

○implementation について

インスタンスを生成した時に自動的に辞書を内部辞書に変換した配列を作らせるようにした。initWithDict: には仮名変換辞書もしくは漢字変換辞書へのフルパスを渡す。最初は innerDict を作って単語のペア(配列)追加していったのだけど、どうやっても空になってしまうので、tmpdict を作ってその結果を innerDict に代入することで作成できるようになった。

_innerDict = tmpdict;return self; の部分は良くわからないで使っている。Xcode がそうしろって表示するからそうしてる。どうもセッターということらしい。return 0; のように 整数を返したら怒られるので self にしただけという。

@implementation MTODict

- (id)initWithDict:(NSString *)dict {
    NSString *filePath = dict;
    .....
    NSMutableArray *tmpdict = [NSMutableArray array];
    .....
            [tmpdict addObject:[result componentsSeparatedByString:@" /"]];
    .....
    _innerDict = tmpdict;
    return self;
}
.....

デバッグは各所に NSLog を設置してそれを見ながらやる方法。ちゃんと辞書が作成されているのか確認するために配列の大きさを表示させていたのだけど、これは収録された語彙を示している訳なので、コマンドラインから確認できるようにメソッドにした。

- (void)printDict {
    printf("辞書の語彙は現在 %lu 語です。\n", [_innerDict count]);
}

変換対象のテキストを読み込んで全文の置換・変換を行うメソッドを作る。car とか cons は癖でそうしているだけで特に意味がある訳ではない。

以前は error を無視するようにしたのだけれど、変換対象のテキストが見付からなかった場合にエラーを表示させたいので利用することにした。これは Go 言語と同じような使い方なのですんなりと使うことができた。

「新仮名→旧仮名、新字→旧字」だけではなく「旧字→新字、旧仮名→新仮名」への変換もしたいので、内容は同じで stringByReplacingOccurrencesOfString:... withString:... へ渡す cons[0] cons[1] を入れ替えただけのメソッド replaceStringCdr: も作ってある。これは辞書を複数作っておくのか、関数を複数作るのかの方針で異なってくると思う。変換するメソッドについても、オプションを付けて if 文で変える方法だってあるし。

- (void)replaceStringCar:(NSString *)file {
    NSError *error = nil;
    NSString *inputFilePath = file;
    NSString *inputText = [NSString stringWithContentsOfFile:inputFilePath
                                                    encoding:NSUTF8StringEncoding
                                                       error:&error];
    if (error != nil){
        puts("ファイルを開けんかったわ");
        return;
    }
    for(id cons in _innerDict){
        inputText = [inputText stringByReplacingOccurrencesOfString:cons[0]
                                                         withString:cons[1]];
    }
    printf("%s", [inputText UTF8String]);
}

○引数処理について

引数処理の部分を作る。main.m でも #import "MTODict.h" することにより MTODict.m に書かれている関数(メソッド)が利用できるというわけやね。

変換辞書の場所は環境変数を利用することにした。環境変数getenv("hogehoge"); で得ることができるのだけど、Xcode 上で実行すると空になってしまうので注意。ここからはいちいちシェル上で clang -framework Foundation main.m MTODict.m -o mtoコンパイルして実行するという方法になる。これ、Cocoa アプリケーションではどうすればいいんだろう?辞書を bundle させちゃうのか、設定ファイルを利用するようにするのかなぁ…

main.m

.....
char *mtodir = getenv("MTODIR");

if (mtodir == NULL) {
    printf("%s", "環境変数 MTODIR を設定してください。\n");
    return 1;
}
.....

引数の数と文字列はそれぞれ argcargv で得られるのでこれを利用して if (argc == 1 || argc >= 4) {...} のような分岐処理をする。argc は数字で argv[0] は文字列。今までは第二引数を変換オプションとして受けて switch 文で処理をしてきたのだけど、Objective-C では switch 文に文字列を使えないことが判明。if 文でやることにした。

で、各オプションでやることは MTODictインスタンス mtoinst を指定の辞書で初期化し、これに変換させたいテキストを渡し、結果を出力させる、というもの。mtoinst = [MTODict alloc] してるのだけど、[mtoinst release] しなくていいのかなぁ…

.....
if (argc == 3) {
    NSString *henkanopt =
    [NSString stringWithCString:argv[1]
                       encoding:NSUTF8StringEncoding];
    if ([henkanopt isEqualToString:@"tradkana"]) {
        dictfile = [NSString stringWithFormat:@"%s%@", mtodir, @"/dict/kana-jisyo"];
        id mtoinst = [[MTODict alloc]initWithDict:dictfile];
        [mtoinst replaceStringCar:[NSString stringWithCString:argv[2]
                         encoding:NSUTF8StringEncoding]];
    }
    else if ([henkanopt isEqualToString:@"modernkana"]) {
        .....

○まとめ

1 ファイルだったものを 3 つのファイルに分けてオブジェクト志向的な方法で作成することができた。Objective-C の作法にもだんだんと慣れてきた。引数を取るような関数の定義とその渡し方の記法が特殊だと感じる。

変換テキストの標準入力からの受付には対応していないけれど、目的とするものができたので、早速ベンチマークをしてみた(結果)。コンパイル型だから…というので期待していたのだけれど、かなり遅いのでがっかりしてしまった。たぶん継ぎ接ぎで作ったプログラムだからいろいろと無駄なことをしているのだろうけど、Python3 の 60 倍も時間がかかってしまっているのだった。これはひどいfor ... in ... が遅いのか stringByReplacingOccurrencesOfString:... withString:... が遅いのか。メモリの開放とかやってないしね…

clang -framework Foundation main.m MTODict.m -o mto でバイナリの作成ができる。

GUI(Cocoa) 版を作る

2014-01-08

プロジェクトの作成。ただそれだけ。クラスのプレフィクスを MTO としたくらいかな。

2014-01-09

ウインドウを作成してその中にテキストボックスを 2 つ配置。それだけ。メッセージのやりとりはどうやればいいのかなぁ……。Visual Studio の場合、Form をダブルクリックすると対応するソースが開かれて、そこにやりたい処理を書いていけば良かったのだけど、Xcode ではどうすればいいんだろうか?

2014-01-10

毎日少しでも触らないといけいないのだろうけど、今日は Objective-C の勉強に関する過去の記録を整理してここにコピペする作業をした。

2014-01-11

Delegate って何だ?

2014-01-12

ここをまとめる作業をしていただけでソースに触れていない……

2014-01-13

どうも Objective-C で検索しても iOS とか iPhone のアプリケーションを作るものしかヒットしないんだわ。CocoaXcode で調べるとそれなりのものが見付かる。

右のペインから Objective-C class を左のペインにドラッグドロップして、AppController.h, AppController.m を作成しなければいけないらしい。また、同じように Object の青い箱アイコンを真ん中のペインの同じものがある部分にドラッグドロップしてカスタムクラスで AppController を選択しておく必要がある。メッセージのやりとりってやつか?

もう少し紙の資料で勉強してからやるかなぁ…

どうも AppController を使うのは古いやり方らしいぞ?自動的に作成される AppDelegate で十分らしい。

2014-01-17

UI の組み立てはフロチャートというか実際に紙に図を書いてメッセージやメソッドの矢印を書くと、その後の実装が楽になるとのこと。

2014-01-19

どうやら IBActionIBOutlet というものを定義して、UI 要素と接続してやる必要があるみたい。