spacemacsのトラックパッドスクロール量を調節する

spacemacsが思いのほか使いやすかったので、これまで育ててきた.emacsをバッサリ捨てて、乗り換えることにしました。思っていた以上にspacemacsは至れり尽くせりで動いてくれたので、

spacemacsはユーザー様の設定を~/.spacemacsの末尾、dotspacemacs/user-configで行いますので(user-initもあるけれども、最後の最後に実行されるのがuser-configの方)、幾つかの設定を選んで持ってくることにしました。

(defun dotspacemacs/user-config ()
  "Configuration function for user code.
This function is called at the very end of Spacemacs initialization after
layers configuration. You are free to put any user code."
  ;; ⌘-↑/↓でファイルの先頭・末尾へ移動
  (global-set-key [s-up] 'beginning-of-buffer)
  (global-set-key [s-down] 'end-of-buffer)

  ;; ⌘-C-←/→でバッファ履歴を行き来する
  (global-set-key [C-s-left] 'previous-buffer)
  (global-set-key [C-s-right] 'next-buffer)

  ;; ⌘-C-↑/↓でヘッダファイルとソースファイルを切り替える
  (global-set-key [C-s-up] 'ff-find-other-file)
  (global-set-key [C-s-down] 'ff-find-other-file)

  ;; ⌘-rでファイルの再読み込み
  (global-set-key (kbd "s-r") 'revert-buffer)

  ;; トラックパッド用のスクロール設定
  (defun scroll-down-with-lines ()
    "" (interactive) (scroll-down 3))
  (defun scroll-up-with-lines ()
    "" (interactive) (scroll-up 3))
  (global-set-key [wheel-up] 'scroll-down-with-lines)
  (global-set-key [wheel-down] 'scroll-up-with-lines)
  (global-set-key [double-wheel-up] 'scroll-down-with-lines)
  (global-set-key [double-wheel-down] 'scroll-up-with-lines)
  (global-set-key [triple-wheel-up] 'scroll-down-with-lines)
  (global-set-key [triple-wheel-down] 'scroll-up-with-lines)
  )

XcodeSafariと同じように⌘-←/→⌘-↑/↓でバッファを移動できるようにすること、トラックバッドの反応がデフォルトで良すぎるため、スクロール量を3行ずつに抑え込んで(人間が追えるスピードで)なめらかにしたことがポイントです。

std::basic_stringの「&foo[0]」は何を返すか

とあるコードを読んでいたら、

std::string foo = "abc";
printf("[%s]\n", &foo[0]); // [abc]

みたいな書き方をしていて「ん?stringなのに??」と思ったので、きちんと調べてみました。結論を書いてしまうと、

というルール2つによって、正しくC文字列のアドレスが返せているとわかりました。 …でも、こういう場合はstd::basic_string::c_str()を使うべきですよね、見にくいったらありません。

(ようやく)SwiftのflatMap()を追いかける

前回(といっても大分前になってしまいましたが)、flatMap==flatten(map())であることから、flatMap()の準備としてflatten()を先に読みました。

swiftlife.hatenablog.jp

Swiftflatten()は平坦化を直ちに行うのではなく、要素が必要になった時にはじめて処理が実行されるよう、遅延評価の仕組みが取り入れられたのでした。この「元配列を別視点から見られるようにする」という仕組みによって、実行コストを抑えたまま平坦化を実現する面白いメソッドになっていることが分かりました。

そんなflattenの振る舞いを踏まえて、今回flatMap()に進みます。SwiftflatMap()は、はたしてどのような実装になっているのでしょうか。

…とその前に、ここまでにmap,flattenを見てきたところとして残っている疑問がありますので、先に書き出しておきます。

  • flatten()の返す型はflatMap()のそれとは異なるが、いつ、どうやって変換しているか
  • flatten()underestimateCount()は常に0を返すけれど大丈夫?

どちらもflatMapを読んで解決できるといいのですが…。それでは読んでみましょう。

flatMap()を提供している型

flatMap()は、Optional, ImplicitlyUnwrappedOptionalSequenceTypeの3つの型で利用できます。ソースコードの定義箇所は、それぞれ以下のとおりです。

  • Optional : stdlib/public/core/Optional.swift
  • ImplicitlyUnwrappedOptional : stdlib/public/core/ImplicitlyUnwrappedOptional.swift
  • SequenceType : stdlib/public/core/SequenceAlgorithms.swift.gyb

さらには、遅延評価型のLazySequenceTypeLazyCollectionTypeでも定義されているので、合計5箇所です。

  • LazySequenceType : stdlib/public/core/flatMap.swift
  • LazyCollectionType : stdlib/public/core/flatMap.swift

OptionalとImplicitlyUnwrappedOptionalのflatMap()

OptionalImplicitlyUnwrappedOptionalの2つは、以前の記事で読んでいるので省略します。

swiftlife.hatenablog.jp

やっていることはとても単純で、アンラップしたオブジェクトに関数を適用して返すことをしていたのでした。

SequenceTypeのflatMap()

SequenceTypeのflatMap()は、SequenceAlgorithms.swift.gybの中で、SequenceTypeのextensionとして一般化された内容で記されています。2つあり、それぞれ短い内容です。

// 1つ目
@warn_unused_result
public func flatMap<S : SequenceType>(
  transform: (${GElement}) throws -> S
) rethrows -> [S.${GElement}] {
  var result: [S.${GElement}] = []
  for element in self {
    result.appendContentsOf(try transform(element))
  }
  return result
}
// 2つ目
@warn_unused_result
public func flatMap<T>(
  @noescape transform: (${GElement}) throws -> T?
) rethrows -> [T] {
  var result: [T] = []
  for element in self {
    if let newElement = try transform(element) {
      result.append(newElement)
    }
  }
  return result
}

ほとんど同じ内容ですが、1つ目がtransform引数の戻り型が「S」であることからnilを返さないこと、2つ目は逆に「T?」となっていて、Optionalで返すことができます。

ここでの${GElement}は、同gybファイルの先頭でGenerator.Elementの言い換えであると定義されているものです。

# We know we will eventually get a SequenceType.Element type.  Define
# a shorthand that we can use today.
GElement = "Generator.Element"

要素をtransform()に渡しつつ、その結果をresultにappend()あるいはappendContentsOf()で追加しています。言うなればflatMap()というよりconcatMap()と表現した方がしっくりくる感じです。

LazySequenceTypeとLazyCollectionTypeのflatMap()

flatMap.swiftという、そのものの名前のファイルでは、LazySequenceTypeLazyCollectionTypeのflatMap()が定義されています。これらは関数宣言が複雑に見えますが、やっていることはどれも同じで次の1行が記述されているのみです。

return self.map(transform).flatten()

これが1番シンプルですね。flatMap()の由来である「flatten(map(x))」そのものになっています。

まとめ

これまで長々とflatMap()に至る実装を追いかけてきましたが、ようやく一区切りつけることができました。

flatten(map(x))が由来のflatMap()ですが、SwiftではLazySequenceTypeとLazyCollectionType以外はflatten()を使っておらず、それぞれの型で異なる実装になっていることがわかりました。実装を区別しているのは、その方がパフォーマンスが良いのかもしれませんし、あるいは疑問の一つだったflatten()の特性「underestimateCount()が0を返す」から、期待する結果をベースに実装を考えているのかもしれません。

細かな不明点・疑問点は残っていますが、それはおいおい調べていこうと思います。

おまけ

RxJS, RxSwiftなどReactive Programmingライブラリのドキュメントで、flatMapを表すとても分かりやすい図があったので引用します。

http://reactivex.io/documentation/operators/images/flatMap.c.png ReactiveX - FlatMap operator

配列(ストリーム)の要素に対して順に処理を実施し、処理では任意型の配列を返します。そして各結果をflattenして、最終的な別のストリームを返します。

SKSceneとUIViewとの座標変換

SpriteKitのSKSceneの座標系は、UIViewの左上座標とは異なり、y軸が反転した左下座標系になっています。そのため、UIGestureRecognizerのようなUIView側の座標系をSpriteKitに持ち込むには、座標系を変換してあげなくてはいけません。

具体的には、SKSceneメソッドconvertPointFromView(_:)convertPointToView(_:)を使います。

// UIView上の座標をSKScene上の座標に変換する
func convertPointFromView(_ point: CGPoint) -> CGPoint

// SKScene上の座標をUIView上の座標に変換する
func convertPointToView(_ point: CGPoint) -> CGPoint

UIPanGestureRecognizerを使ってスプライトをドラッグしようとしていて、ハマりかけました。結局、その時のコードは次のようになりました。

    @IBAction func panned(recognizer: UIPanGestureRecognizer) {
        let location = recognizer.locationInView(_spriteView)
        // タッチ座標をSKScene上の座標に変換。これを忘れていた
        let sklocation = _spriteView.scene!.convertPointFromView(location)

        if let node = _spriteView.scene?.nodeAtPoint(sklocation)
            where node is SKSpriteNode
        {
            // ノードのイベント生起
        }
    }

svn commit時に英語でメッセージ書くためのヒントを表示する

Qiitaに次のような投稿がありました。

qiita.com

git commitをするときに英語の例文をコメントとして表示するようにして、英語コミット力を上げようという内容です。これはいいなと、同じようなことがSubversionでも出来ないかなと調べてみました。結果としてうまくできたので、今回はそのときの調査メモと、実現方法を記します。

調査編

テンプレートが指定できない?

svnのオプションなどを調べ始めてすぐに気付いたのですが、Subversionにはそもそもテンプレートを表示する設定・外部変数のような概念は存在していないようです。環境変数SVN_EDITOR」を使い、コミットで使うエディタを指定できる程度で、それ以上のカスタマイズはできません。ネット上の情報にも当たってみましたが、ほとんどがエディタのコマンドだけを指定しているだけで、特殊変数のようなものは使っていませんでした。

とはいえ、コミット時のエディタ上には対象ファイル/ディレクトリの一覧と、入力欄とを区切る一文が表示されています。これはどのようにして渡ってきているのかと、以下のようなシェルスクリプトを作成して引数を調べてみました。

#!/bin/sh

echo "num args: $#"
for arg in $@; do
        echo "-----"
        echo $arg
done
env
exit 0

コマンド引数の数と、その内容、そして環境変数を出力するだけのスクリプトです。これをSVN_EDITORに指定してsvn commitを実行してみると…

$ SVN_EDITOR=/Users/user/bin/commit.sh svn commit
num args: 1
-----
svn-commit.tmp
  :
…環境変数の一覧…

というように、ファイル名として「svn-commit.tmp」が指定したプログラムに渡されていて、それ以外は環境変数も含めて何もしていないことがわかりました。ちなみにsvn-commit.tmpはコミット作業中にしか存在しない一時ファイルで、中身はコミット時に出ているメッセージそのものでした。

% cat ~/tmp/foo/svn-commit.tmp

--This line, and those below, will be ignored--

…更新ファイルの一覧…

しかもこの「svn-commit[.XX].tmp」という名前はSubversionのソースにハードコーディングされていて(!)、気軽に変更できるようなものではありませんでした。

 if (! lmb->non_interactive)
   {
     err = svn_cmdline__edit_string_externally(&msg_string, &lmb->tmpfile_left,
                                               lmb->editor_cmd,
                                               lmb->base_dir ? lmb->base_dir : "",
                                               msg_string, "svn-commit",  // ←ココ!
                                               lmb->config, TRUE,
                                               lmb->message_encoding,
                                               pool);
   }
 else /* non_interactive flag says we can't pop up an editor, so error */
   {

…番号で重複を避けているとはいえ、このカスタマイズを一切許さない設計はどうなんだろう。

ともかく、gitと同じ方法では実現できなさそうです。

どうやってコミット内容だけを取り出しているか

それではと、次にコミットファイル「svn-commit.tmp」の内容に注目し、このファイルからどうやってコミットメッセージだけを取り出しているかを調べてみることにしました。ファイル内には「--This line, and those below, will be ignored--」から始まる文字列が含まれていますが、コミット後のログメッセージにはその内容が一切含まれていません。ということは、コミット処理のときに何かしらの方法で取り除いているはずです。

それが分かれば、gitの方法#で始まる行が無視されることを利用して例文を挿入しているのと同じことが実現できるはず、という魂胆です。

本ファイル作成処理は先ほど述べたようにSubversionソースコードとして書かれていたので、どうせその近辺に処理があるだろうと読んでみます。

すると、trunk/subversion/svn/util.cの337行目にて、svn-commit.tmpに書かれている「--This line, and those below, will be ignored--」そのものがマクロで定義されていました。当然のようにハードコーディングされています。ローカライズの余地すらありません。

#define EDITOR_EOF_PREFIX  _("--This line, and those below, will be ignored--")

さらに読んでみると、この行より後ろに書かれた内容はすべて切り捨てていて、一切無視していることがわかりました。最初に現れるこの行以降はすべて無視される仕様です。

なるほど。

調査のまとめ

ここまでの調査をまとめます。

  • subversionではgitにあるようなテンプレートを指定する設定はない
  • エディタは環境変数SVN_EDITORで指定できる。…が、第1引数にファイル名svn-commit.tmpが渡されるだけ
    • ファイルの中身は、どのファイルが更新されるかが書かれた、いつものメッセージ
  • ファイル名、ファイルの内容はソースコードにハードコーディングされていて、再コンパイルしなければ変更できない
  • コミットメッセージとコメントは、コミットメッセージの最初に現れる「--This line, and those below, will be ignored--」行よりも前か後ろかで判断する

ということで、ファイル名と魔法の文字列とがキーになることがわかりました。ここまで分かれば色々とできそうです。

実現編:例文コメントの 実現

というわけで、調査結果をふまえると、次のような方針であれば実現出来そうだと期待できます。それは、

環境変数SVN_EDITORにsvn-commit.tmpを書き換えた上でエディタを起動するコマンドをいれ、コメントにしたい文は"--This line, and those below, will be ignored--"の後に入れておく」

という方法です。svnプログラムが把握しているのはコミットメッセージの対象ファイルのみですから、エディタが終了するまでの間に適当なファイルないように書き換えてしまえば、コメントだろうとテンプレートだろうと、こちらの狙ったメッセージを入れ込むことができるだろうと考えました。

さっそくそんな感じの処理を実現する以下のスクリプトを書いてみました。

#!/bin/sh

mv "$1" "$1".orig
echo "\n--This line, and those below, will be ignored--" > "$1"
echo "Hello, world!" >> "$1"
cat "$1".orig >> "$1"
rm "$1".orig
vi "$1"

exit 0

まず元のファイルを.origと付けて退避しておきます。そして、魔法の言葉「--This line, and those below, will be ignored--」を挟んでからコメント(ここではとりあえず"Hello, world!")を入れ、最後に退避しておいた元のデータを書き加え、退避データは削除します。

・・・さあ、上記のファイルを~/tmp/commit.shに保存して、うまく動くかどうか試してみましょう。

(SVN_EDITOR=~/tmp/commit.sh svn commitの実行画面)
コミットメッセージを書いてみる

--This line, and those below, will be ignored--
Hello, world!

--This line, and those below, will be ignored--

M    myfile

おお。

% svn up
Updating '.':
At revision 4.
% svn log
------------------------------------------------------------------------
r4 | XXXXX | 2016-01-23 14:02:30 +0900 (土, 23  1 2016) | 2 lines

コミットメッセージを書いてみる
------------------------------------------------------------------------
:

おおお。うまくいきました!

まとめ&出来上がったスクリプト

当初はsvnでは環境変数がないため実現できないかも、と思った今回の調査でしたが、SVN_EDITORに渡すコマンドとコメント判別ルールを利用することで、git版と同様の機能を実現することができました。

最後に、svngit版の記事と同じようなコミットコメントを実現するスクリプトを記します。先ほどのテストでは「-- This line, …」が2回も出てきてうっとうしかったので、それを消すような処理も加えてあります。

#!/bin/sh

echo "num args: $#"
for arg in $@; do
    echo "-----"
    echo $arg
done

mv "$1" "$1".orig

# コミット時のコメントを作成
cat <<'_EOT_' > "$1"

--This line, and those below, will be ignored--

fix, add, changeといった事実ではなく、このcommitで実現する要件や仕様を書きましょう。(リファクタなどは除く)

 例文)
 - Fix typo in docs
 - Remove unused code
 - Remove use of deprecated method
 - Update Modernizr to v1.6
 - Make it possible to have IDs per request
 - Make sure to reset default_url_options(必ずリセットする)
 - Don't use "assert_not_nil"
 - Allow the user to drag faster
 - Remove methods to avoid warnings.
---
_EOT_

# 元コメントの3行目以降(=変更ファイル一覧)を追加
tail -n +3 "$1".orig >> "$1"
rm "$1".orig

# エディタの起動
vi "$1"
exit 0

これでsvnしか使えないプロジェクトでも幸せになれますね!

NSNumberで扱われるSwiftの数値型

SwiftのIntやFloatのようなプリミティブな数値型をCやObjective-Cとで相互に扱う場合、その型は基本的に同じ値域の型で表されます。具体的には「Using Swift with Cocoa and Objective-C (Swift 2.1)」の「Interacting with C APIs」-「Primitive Types」にある、以下の表の通りです。

Cでの型 Swiftの型
_Bool,bool(C++) Bool, CBool
char, signed char Int8, CChar
unsigned char UInt8, CUnsignedChar
short Int16, CShort
unsigned short UInt16, CUnsignedShort
int Int32, CInt
unsigned int UInt32, CUnsignedInt
long Int, CLong
unsigned long UInt, CUnsignedLong
long long Int64, CLongLong
unsigned long long UInt64, CUnsignedLongLong
wchar_t UnicodeScalar, CWideChar
char16_t UInt16, CChar16
char32_t UnicodeScalar, CChar32
float Float, CFloat
double Double, CDouble

表の多くの項目で複数の型が書かれているのは、「C〜」で始まる型がそうでない型のtypealiasであるためです。

このように相互変換の可能な型が一通りそろっているものの、SwiftにはC言語にあるような暗黙の型変換がないため、Cの慣用的な型の考え方以上に型を強く意識すること(あるいはIntでまとめてしまうこと)が求められます。また、SwiftのIntはCのlong、CのintはSwiftだとInt32(またはCInt)に相当しているので混乱しないよう注意すべきです。

ところで、Objective-Cでは数値をオブジェクトとして扱うための型「NSNumber」があります。この型は、Swiftでは数値としてアンラップした状態で利用できます。「Using Swift with Cocoa and Objective-C (Swift 2.1)」から引用すると、以下の5つの型がNSNumberで扱われます。

  • Int
  • UInt
  • Float
  • Double
  • Bool

gitbookでPDF/epub出力できるようにする(calibre)

前回の記事によりmarkdownファイルをgitbookでHTML化できるようになったわけですが、せっかくなのでPDFでも出力できるようにします。

gitbookからPDF化するにはCalibreを使う

gitbookのgithubページには、eBookへの出力について次のように書いてあります。

eBook: You need to have ebook-convert installed. You can specify the eBook filename as the second argument, otherwise book will be used.

gitbookではコンバートに「Calibre」というツールに含まれる「ebook-convert」コマンドを使うとのこと。CalibreはGUIを持った高機能な電子書籍管理アプリで、コンバート機能は補助的なもののようです。

calibre - About

calibre is a free and open source e-book library management application developed by users of e-books for users of e-books. It has a cornucopia of features divided into the following main categories:

参考までにWikipediaからも引用。

calibre - Wikipedia

Calibreとはフリー・アンド・オープンソース電子書籍ソフトウェアであり、電子書籍を保存や管理を行うことが可能で、多数のフォーマットに対応している。またDRMのかかった電子書籍を他のフォーマットへ変換でき、数種類の主な電子ブックリーダーと同期することができる。

Calibreのインストール

ということで、Calibreをインストールします。ダウンロードページからOS X版のインストーラが入ったdmgファイルをダウンロードします。バージョンは2.49.0でした(2016/01/20時点)。dmgの中には.appファイルが入っているので(ちゃんと署名されていました)、/Applicationsにコピーするだけです。

f:id:swiftlife:20160120132017p:plain

Calibreの初回起動時は管理ディレクトリやcontent serverの起動を尋ねられますが、今回はコンバートにだけ利用する予定なので、何もさせないよう適当な値を入れつつインストールしました。ただしそれだけだと目的のebook-convertコマンドが使えないので、calibre.app下にパスを通しておきます。

export PATH=/Applications/calibre.app/Contents/MacOS:$PATH

あ、公式ドキュメントの「2.3 ebook-convert」にもパスの通し方について記載がありました。こちらはebook-convertコマンドだけをシンボリックリンクで外に出していますね。

Mac

Download the Calibre app. After moving the calibre.app to your Applications folder create a symbolic link to the ebook-convert tool:

$ sudo ln -s ~/Applications/calibre.app/Contents/MacOS/ebook-convert /usr/bin

You can replace /usr/bin with any directory that is in your $PATH.

どちらでも好きな方でよいと思います。 以上で準備完了です。

PDFへのコンバート

コンバートするには、gitbookコマンドに「pdf」オプションを付けるだけです。htmlのときのbuildオプションの代わりに指定します。

% gitbook help
  build [book] [output]      build a book
    --format     Format to build to (Default is website; Values are website, json, ebook)
    --log    Minimum log level to display (Default is info; Values are debug, info, warn, error, disabled)

  pdf [book] [output]    build a book to pdf  ←PDF化はこちらを使う
    --log    Minimum log level to display (Default is info; Values are debug, info, warn, error, disabled)

簡単に試してみました。

$ mkdir ~/mybook && cd ~/mybook
$ gitbook init   # 初期化(README.mdとSUMMARY.mdの作成)
$ gitbook pdf  # PDF化
$ ls
README.md  SUMMARY.md book.pdf

book.pdfが出来上がっていることを確認できました。

追加調査とまとめ

簡単な文章で試したところ、日本語はおおむね大丈夫でしたが絵文字「😄」はHTMLと異なり空白で表示されるだけでした。

しかし簡単な作業でそこそこ整ったPDFができるので、これは便利そうです。