無効値の話①

moriyoshi.hatenablog.com

先日このような駄エントリを書いた。

十分ユニットテストがあればきっと防げたであろう問題であるが、それよりも、

なぜnilが必要だったのか

という点で疑問を感じるコメントが散見されたので、その点を補っておきたい。

このプロジェクトではGORMというORマッパー的なものを利用しており、このマッパーを使う限りにおいては、モデルの中でSQL null (NULL) が与えられうるカラムに対応するフィールドは sql.NullString を用いるか値型をそのポインタ型にする必要がある。

type User struct {
    Id int
    Name sql.NullString
}

もしくは

type User struct {
    Id int
    Name *string
}

どちらを採用すべきかという点では議論がある。型のセマンティクスという観点では、SQLの文字列型は母言語の文字列型とは意味論的に違いがあるものなので、前者のように専用の型をあてがうのが妥当と言える。一方で、ORマッパーというからには、データストアの詳細ができるだけモデルに露出しないほうが望ましいという考え方がある。件のプログラムは、後者の観点で、ポインタを使って基本型の値型の NULL を表現していたのである。

encoding/jsonJSON のオブジェクトと struct で表現されたモデルを対応付けする場合にも同じような問題がある。

nullが来ないバージョン:

package main

import (
    "fmt"
    "encoding/json"
    "bytes"
)

type User struct {
    Id int `json:"id"`
    Name string `json:"name"`
}

const jsonStr = `
{
  "users": [
    {
      "id": 1,
      "name": "foo"
    }
  ]
}`

func main() {
    r := bytes.NewReader([]byte(jsonStr))
    val := struct {
        Users []User `json:"users"`
    }{} 
    err := json.NewDecoder(r).Decode(&val)
    if err != nil {
        panic("oops")
    }
    fmt.Printf("%v\n", val)  
}

https://play.golang.org/p/gVayB9HDinj

nullを許容するバージョン:

package main

import (
    "fmt"
    "encoding/json"
    "bytes"
)

type User struct {
    Id int `json:"id"`
    *Name string `json:"name"`
}

const jsonStr = `
{
  "users": [
    {
      "id": 1,
      "name": "foo"
    },
    {
      "id": 1,
      "name": null
    }
  ]
}`

func main() {
    r := bytes.NewReader([]byte(jsonStr))
    val := struct {
        Users []User `json:"users"`
    }{} 
    err := json.NewDecoder(r).Decode(&val)
    if err != nil {
        panic("oops")
    }
    fmt.Printf("%s\n", *val.Users[0].Name)
}

https://play.golang.org/p/bcKXLM8UoN1

encoding/jsonのマーシャリングでも、JSONの文字列型を表現する専用の型を用意してあてがうことは可能だ。しかし、それをしなくても、上記のように値型のポインタを置いておくことでJSON nullに対応付けることもできる。

さて、これまではあくまで母言語と他方言語の双方に、組み込みの値システム / 型システムにおいて、無効値に相当するものが定義されていた場合に、どう対応付けを行うかという視点で考えてきた。

しかし、アプリケーションの設計という意味では、また別の観点もある。SQLであろうとJSONであろうと、母言語のデータ構造との1-to-1マッピングを前提とするならば、そもそもセマンティクス上の不整合が出るようなスキーマ設計をしないということものだ。具体的に言えば、SQLであればNOT NULL制約を付加すべきで、JSONであれば、null値が来るような仕様にしないといった具合である。

無効値と一口に言っても、無効であることが使い手によって明示された結果としての無効値であるのか、実行環境において、何らかの導出の結果として生じた「値が得られない」という事象を指し示す表示としての無効値なのか、の区別があり、特にSQLに関していえば、外部結合などで現れることからわかるように、NULL は後者の意味合いが強い。一方JSONに関しては、それが由来した言語であるJavaScriptのセマンティクスを見る必要があるが、JavaScriptは動的型付け言語で、そこでは複合型 (arrayobject) における要素型に前提を置いていないことにより、ある要素が取りうる値域を複数の型で多重化することが可能なため、null 自体はobject型の無効値であるにもかかわらず、使い手が汎用的に無効値を表す指標として利用可能である一方、object型の操作で後者を表現するものとして undefined という特別な組み込み型が用意されていることから、JSONにおけるnullの役割は、前者を示唆するものとなっている。

Goに立ち返ってみると、静的型付け言語であるGoにおいてはnilは値域として無効値の存在を許されたそれぞれの型 (スライス型、ポインタ型、interface型) ごとに存在するものとして位置づけられている *1

このように、他方言語として取り上げている2つに関してだけでも、何も対処をせずに不整合を避けることはできず、そのような観点にも一理あるといえる。

(たぶん続く)

*1:ただし、異なるinterface型や異なるポインタ型同士の関係演算は定義されていて、異なるnil同士の同値関係が成立する。また、JavaScriptのnull定数と違い、定数としてのnilは型を持たず、文脈によって型が決まるようになっている

nilが欲しいからと言ってむやみにポインタを使うとひどい目にあう話

スマートフォンアプリ側の実装担当として少し関わっているあるβ版のサービスで、奇妙だが比較的クリティカルな問題が発生していた。

それもメッセージが関係ない人間に届くというものであった。

なんとなくその問題を担当することになったが、常に状況が再現するわけではなく、他の仕事もあったので、問題への見当を付けられないまましばらく過ごしてしまった。

そのメッセージを送信する部分はサーバサイドのコードで、Goで書かれていた。

そのコードのエッセンスを抽出すると、以下のようになる。

package main

import (
    "fmt"
    "sync"
)

type User struct {
    Id   int
    Name string
}

type Mail struct {
    Title string
    UserId *int
}

func sendMail(mail Mail, wg *sync.WaitGroup) {
    fmt.Printf("Title=%s, UserId=%d\n", mail.Title, *mail.UserId)
    wg.Done()
}

func main() {
    users := []User{
        User{Id: 1, Name: "AAA"},
        User{Id: 2, Name: "BBB"},
    }

    wg := &sync.WaitGroup {}
    for _, user := range users {
        mail := Mail{
            Title: fmt.Sprintf("%s, check out our season offerings!", user.Name),
            UserId: &user.Id,
        }
        wg.Add(1)
        go sendMail(mail, wg)
    }
    wg.Wait()
}

The Go Playground

sendMail() に渡される引数であるところの Mail の値はループの中で都度作られているので一見よさそうに思えるのだが、実行してみると、時として次のような結果になるのだ。

Title=BBB, check out our season offerings!, UserId=2
Title=AAA, check out our season offerings!, UserId=2

(Goroutineのyieldするタイミングによって結果が変わるので、期待値通りになることもある。)

もちろん、期待値は

Title=BBB, check out our season offerings!, UserId=2
Title=AAA, check out our season offerings!, UserId=1

のように、タイトルに含まれるユーザ名とユーザのidが一致することである。

あな恐ろしや…!!!

原因は、 MailUserId として、ループ外のスコープで定義された userId メンバへのポインタが与えられ続けるためだ。

「ループ外」と書くと、「userはfor文より外のスコープで扱えないわけだからループの中だろ」というツッコミが入りそうだが、実際のスコープの内実はこうだ。

  • for文の外側のスコープ
    • for文の宣言のスコープ (for ... := ... の部分)
      • for文の内側のスコープ ({ } の中)

この中間のスコープはブレース (中括弧) で囲まれていない範囲なので意識しづらいのだ。

さて、このコードは取り急ぎ以下のように書き換えればよい。

package main

import (
    "fmt"
    "sync"
)

type User struct {
    Id   int
    Name string
}

type Mail struct {
    Title string
    UserId *int
}

func sendMail(mail Mail, wg *sync.WaitGroup) {
    fmt.Printf("Title=%s, UserId=%d\n", mail.Title, *mail.UserId)
    wg.Done()
}

func main() {
    users := []User{
        User{Id: 1, Name: "AAA"},
        User{Id: 2, Name: "BBB"},
    }

    wg := &sync.WaitGroup {}
    for _, user := range users {
        userId = user.Id
        mail := Mail{
            Title: fmt.Sprintf("%s, check out our season offerings!", user.Name),
            UserId:  &userId,
        }
        wg.Add(1)
        go sendMail(mail, wg)
    }
    wg.Wait()
}

The Go Playground

書き換えればよい、とは書いたものの、こういうコードにすべきじゃないというのが正直なところだろうか。

「ユニコード」で予期せぬ目に遭った話

などというツイートをしたところ、思ったより反響があったのでまとめておく。

上記ではあいまいに「バリエーション」などと書いたが、Unicodeとそれを扱う環境においては、バリエーションと一口に言っても次のような状況がある。

  1. 意味論的に等価な異なる字形の集合
  2. 同じ字形で異なるコードポイントの集合

aは結構なじみ深いと思う。

a-1. 異なるコードポイントにそれぞれ異なる字形が割り当てられているもの

例:

  • 「東」(U+6771) ⇔「东」(U+4E1C)
  • 「斉」(U+6589) ⇔「齊」(U+9F4A)
  • 「高」(U+9AD8) ⇔「髙」(U+9AD9)

a-2. 同じコードポイントで、フォントによって字形が異なるもの

例:

  • MS MinchoのU+9AA8」(MS MinchoのU+9AA8) ⇔「SimSunのU+9AA8」 (SimSunのU+9AA8)
  • EmojiOneのU+1F600」(EmojiOneのU+1F600) ⇔「NotoのU+1F600」 (Noto Color EmojiのU+1F600)

a-3. 同じコードポイントで、異体字セレクタなどによって字形が特定されるもの

例:

  • EmojiOneのU+260E」(U+260E + VS16)⇔「MS MinchoのU+260e」(U+260E + VS15)
  • 「辻󠄁」(二点しんにょう U+8FBB + VS18) ⇔「辻󠄀」(一点しんにょう U+8FBB + VS17)

bの状況としては、

b-1. 意味論的にも同じだが政治的・歴史的経緯で重複して収録されているもの

例:

  • 「A」(全角アルファベットのA、FULLWIDTH LATIN CAPITAL LETTER A U+FF21)⇔「A」(アルファベットのA、LATIN CAPITAL LETTER A U+0041)
  • 「א」(ヘブライ文字アレフ U+05D0)⇔「ℵ」(アレフシンボル U+2135) *1

b-2. 意味論的に異なっているため包摂されず別個に収録されているもの

例:

  • 「西」 (WEST U+897F) ⇔「⻄」(CJK RADICAL WEST TWO U+2EC4)

さて、ツイートしたのは、b-2 の状況である。CJK Radicalsとは「部首」を表す文字のことで、意味論的には単一の部首のみで構成されるCJK Ideographとは区別される。

これの何が恐ろしかったかというと、現実に (とあるインターネットサービス上で) ユーザーが住所の一部に入力した文字としてこのCJK Radicalsが出現し、CP932に変換できないという事案が発生したからだ。

JISX0208とJISX0213で例示字形が異なることからWindowsのバージョン間でもフォントの差異があり、しばしばやり玉に上げられる「辻」などでも、最近のOS標準のIMEがIVSにも対応していることから、同様の問題が起こりそうな予感がする。やれやれ。


*1 両者が意味論的に等価かどうか、というのは議論の余地がある、たとえば「R」と「ℝ」は文脈の区別をしたいからこそ字形が異なっているわけなので。しかし、ここではdecomposed formがあるかないかを基準とすることにした。

mod_wozozo

(この記事は、Pyspaアドベントカレンダー 2016の12/4のコミットを補足する形で書かれました。)

はじめに

皆様ご存知のことと思いますが、OSSコミュニティーにおいて、開発者が結婚する際には、新郎と新婦に

オリジナル○○○○○○○○○○○

を贈呈するという慣習があります。

はい、もちろん

オリジナルApacheモジュール

ですね。

タイムライン

10/3

ハンドルwozozoだしやっぱりOzネタで行くしかないだろうということで、OzのApacheモジュールを実装することにする。

12/4

11:00

起床。全日の京都での披露宴参加からの帰宅で思いのほか疲れており、8時起床予定が大幅にずれる。

11:30

鬼平犯科帳スペシャルを見始める

13:40

鬼平犯科帳スペシャルを見終わる。そろそろアドベントカレンダーのネタ仕込みするかと思い、github.com:mozart/mozart2 をチェックアウト

13:45

ビルドインストラクションを見る。とりま cmake などを入れてみて、 cmake . などする。

13:50

cmake が通らない。自動的に clang とか LLVM とか落としてくると書いてあるのだが... 仕方なく OS (Ubuntu 15.04) のパッケージから LLVM を入れてビルドを試みることに。

14:00

OS のパッケージの clang や LLVM では、なぜかインストールされるファイルの欠損がありビルドができない。Ubuntu 16.10 では直っているようなので、do-release-upgrade を試みる。

14:20

OS のアップグレードが完了したので、ビルドを開始する。

14:30

ビルドに失敗するので、いろいろ調べてみると、ライブラリ付属の cmake ファイルの甘さがいくらかあることが判明。どのオプションが必須なのかを調査する。

14:40

オプションの指定を解明できた。ビルドが走り始める。時間がかかりそうなので、食事に出かける。

15:00

食事から戻る。ビルドは無事完了していた。ランタイムのテストをするため、Oz の言語仕様などを調べ始める。

15:15

古いバージョン (Mozart1) の資料からコピペしてサンプルコードを動かすことに成功。

15:30

標準出力に出力する方法 (System.show) などを学ぶ。

16:00

ソースコードの構成を学びはじめる。

17:00

VMのフロントエンドとしてBoostにプラットフォームの差異を吸収させたboosthost と、それの対になる boostenv というコンポーネントがあることがわかる。この部分で切り出すことでApacheモジュールに組み込めそうだという見込みを立てる。

17:10

Apacheモジュールのひな形を書き始める。apxsはそのまま使えないのでapxsから変数だけクエリしてあとはMakefileでやることにする。

18:30

催促される。

19:00

libtool に -module オプションを渡しても .so が生成できない問題の謎が解ける。 (-rpath オプションが必要だった)

19:30

ひな形にMozart2のコードの一部を組み込んだ形でコンパイルを通すことに成功する。

だが、Mozart2のビルドシステムでビルドされるコンビニエンスライブラリ (最終的なバイナリ生成物である ozemulator を生成するためにビルドの途中で便宜的に作られるライブラリ) は、PIC コードではなかったのでリンクで失敗してしまう。

仕方ないので CMAKE_CXX_FLAGS-fPIC を追加した形で Mozart2 をビルドしなおすことに。ビルドに時間がかかるので食事に出かける。

20:00

食事から戻り、作業再開。すぐにビルドには成功する。

Apacheのconfigから得られた内容をVMの起動パラメータに変換する簡単なお仕事の開始。

21:00

I/Oスレッドの扱いに悩み始める。I/Oスレッドをひとまずpost_configフェーズで起動する形で実装。

22:00

handlerVMを起動させるところまで来る、が、VMが起動しない。デバッグを続ける。

23:40

ドはまりしたが、余計なパラメータを渡していることが原因と判明、修正し、リクエストURIで指定されたドキュメントルート配下のMozart2のバイナリを読み込んで実行することに成功する。しかし、この時点では、まだレスポンスを書き出すことはできていないので、標準エラー出力への実行結果の出力で確かめるのみ。

24:00

Apacheとのつなぎこみを始める。最初の難関である、VMの起動時にVMにrequest_recを渡すという部分への対応を開始する。

25:20

VMのパラメータとして渡しているのは、値渡しのVMIdentifier VirtualMachineOptions型と、 std::unique_ptr<std::string> 渡しのアプリケーションURLのみ。

std::string を継承したクラスを作り、そこに request_rec を突っ込むという強引なやり方で成功する。

25:30

Mozart2 のビルトインプロシージャの作り方を調べる。C++のクラスの実装と、それをビルドシステムのブートストラップでclangのAPIを使ってパースしてスタブを生成するコードから成ることを解明して、スタブを自前で書くことで対応を開始する。

26:00

レスポンスを書き出すビルトインプロシージャを作るには、VM生成時に渡されたrequest_recをプロシージャの文脈で取得できなければならないが、しかし、エントリポイントは、スタティックメソッドである。

VMからビルトインモジュール名を指定してモジュールを取得できそうなことが分かったので、その方法を調べる。

27:10

vm/vm/main 配下のVM内部の実装を読み解き、ようやくモジュールを取得してもコンテキストは取れないので、代わりにビルトインプロシージャのスタブそのものへのポインタを取得する方法しかないことが判明する。

27:40

Apache.Rputs の実装ができ、ブラウザに文字を出力することに成功する。疲弊しきったため風呂に入る。

28:00

1時間ほどの仮眠を試みるが、4時間寝てしまう。

32:00

Ozで何をするかを決めようとする。I/Oスレッドが独立していることから、それが正しく動いていることを確認するためのコードを書きたく、それをそのままネタにすることにする。

32:10

TCPでの通信の方法を調べはじめる。とりあえずApacheの文脈から外れて、Ozでスタンドアロンで通信をするコードを書く。サーバー側はGoで実装した。

33:10

Goで実装したサーバーとの通信に成功する。

33:20

通信した内容をApacheを通じてレスポンスとして返すことに成功する。 ネタを仕込むための実装を始めるため、Ozの標準モジュールについていろいろ調べ始める。

33:40

ちょっとした遊びを実装するため、Apache.Rflush の実装をする。

34:10

通して動くようになる。レポジトリを作成して、push。

34:54

最後に

めでたい。

Remove all ads

FFmpegで動画編集をするガイド

こんにちは。年末になり、ますますコマンドラインで動画編集をする機会が増えてきているかと思いますが、皆様いかがお過ごしでしょうか。普段触れていないとついつい忘れてしまうffmpegのオプション群。そんなあなたのためのチートシートです。

基礎編

トランスコーディング

ffmpeg -i input.mp4 -c:v libx264 -preset medium -c:a libfdk_aac output.mp4

または

ffmpeg -i input.mp4 -vcodec libx264 -preset medium -acodec libfdk_aac output.mp4

解説

-c:v オプションは映像コーデックを、-c:a は音声コーデックを指定する。古くはそれぞれ -vcodec -acodec というオプションであった。両者は等価である。

-preset は libx264 の設定プリセット名を与える。

AAC エンコーダは ffmpeg ネイティブのものもあるが (aac) 2015年12月現在、未だ実験的 (experimental) な扱いとなっているため、libfdk_aac を代わりに使っている。なお、デコーダは experimental ではない。

(もしネイティブの aac コーデックを利用する場合には -strict experimental オプションが追加で必要となるので注意)

コンテナフォーマットの変更

ffmpeg -i input.flv -c:v copy -c:a copy output.mp4

または

ffmpeg -i input.flv -c:v copy -c:a copy -f mp4 output.mp4

解説

出力ファイルフォーマットは拡張子から自動的に決定される。-f オプションにより明示することもできる。コーデック copy を指定すると、入力ストリームはエンコード・デコードを経ずにそのまま出力に流される。

映像から音声を抜き出す

ffmpeg -i input.mp4 -c:a copy -vn output.mp4

または

ffmpeg -i input.mp4 -c:a copy -map 0:a output.mp4

解説

コンテナフォーマットに依存するが、基本的には動画ファイルは1つ以上の映像ストリームと音声ストリームから構成されている。何も指示がない場合、ffmpeg は入力のストリームの構成から自動的に出力のストリームの構成を決定する。入力ストリームと出力ストリームの対応を明示的に行うには -map オプションを用いる。-map オプションを指定した場合、対応づけが行われていないストリームは破棄される。

-map オプションに指定できるのは次のような識別子である。

識別子 説明
0:0
1:1
0番目の入力ファイルの0番目のストリーム
1番目の入力ファイルの1番目のストリーム
0:v
1:a
0番目の入力ファイルの全ての映像ストリーム (出現順)
1番目の入力ファイルの全ての音声ストリーム (出現順)
[out] libavfilter (lavf)のストリーム名 (後述)

動画に別録りした音声をつける

ffmpeg -i input.mp4 -i audio.mp3 -c:v copy -c:a aac -strict experimental -map 0:v -map 1:a output.mp4

解説

ffmpeg では -i オプションを繰り返し指定することで、複数の入力ファイルを指定できる。最初の -i オプションで指定したファイルは 0 番目のファイル、次の -i オプションで指定したファイルは 1 番目のファイルとなる。

この例では、前述の -map オプションを用いて、input.mp4 ファイルの動画ストリームと audio.mp3 の音声ストリームを合わせて output.mp4 として出力している。

動画のリサイズ

ffmpeg -i input.mp4 -s hd480 output.mp4

または

ffmpeg -i input.mp4 -s 852x400 output.mp4

または

ffmpeg -i input.mp4 -vf scale=hd480 output.mp4

または

ffmpeg -i input.mp4 -vf scale=852:400 output.mp4

解説

-s オプションは出力される動画ストリームのフレームサイズを指定する。一方、-vf オプションは動画ストリームに適用するフィルタを設定する。

明示的な指定がなくとも、常に暗黙にスケーリングフィルタが適用されるので、-s オプションの指定で大体は十分ではあるが、スケーリングフィルタを明示的に指定することで、細かな設定が可能になる。

(実は上の例で-vfオプションを指定した場合には明示的に指定されたフィルタの上に等倍のスケーリングフィルタが適用されることになり、二重にスケーリングフィルタが適用されることになるが、問題になることはない)

動画から連番のPNGファイルを生成

ffmpeg -i movie.mp4 -c:v png -f image2 output/%06d.png

解説

出力フォーマット image2 では %XXd のようなプレイスホルダーをファイル名に含めることで、フレーム番号をファイル名に含めることが可能である。

動画からアニメーションGIFを生成 (thanks to Yoshiori)

ffmpeg -i movie.mp4 -c:v gif -f gif -r 8 -s 320x180 output.gif

解説

出力フォーマット gif を使うとアニメーションGIFを生成することができる。元動画のフレームレートが高い場合、出力ファイルサイズが肥大してしまうかもしれない。そのような場合は -r オプションを指定することで、出力ファイルのフレームレートを指定して、調整を図る。

動画の特定の範囲を切り出す

ffmpeg -i movie.mp4 -ss 10 -t 20 -c:v copy -c:a copy output.mp4

解説

-ss オプションを指定すると、続く引数に指定された秒数にシークして変換を開始する。-t オプションは、続く引数に指定された秒数、変換を行う。

応用編

動画に画像を重ねる

ffmpeg -i movie.mp4 -i picture.png -filter_complex '[0:0] [1:0] overlay=x=0:y=0 [out]' -map '[out]' -map 0:a output.mp4

解説

overlayフィルタを利用すると、フィルタの0番目の入力を1番目の入力に重ねることができる。

この例では、最初の -i オプションで指定された映像のストリーム ([0:0]) をフィルタの0番目の入力に、次の -i オプションで指定された映像のストリーム ([1:0]) をフィルタの1番目の入力として、1番目の入力を0番目の入力のフレーム上の座標 (x, y) = (0, 0) に重ねている。

指定したタイミングで映像に画像を重ねる

ffmpeg -i movie.mp4 -i picture.png -filter_complex '[0:0] [1:0] overlay=x=if(gt(t,5),if(lt(t,10),0,-10000),-10000),y=0 [out]' -map '[out]' -map 0:a output.mp4

解説

overlayフィルタのパラメータにはexpressionを使うことができる。この例では現フレームの時間であるtという変数を使ってオーバーレイする座標を非連続に設定している。

5秒未満: x=-10000 10秒未満: x=0 10秒以上: x=-10000

重ねる対象を x=-10000 のように画面外に持ってくることによって特定のタイミングで重ね合わせを行うことを実現している。

まとめ

とくにまとめるような内容ではないのですが、よいお年をお迎えください。

Many thanks to pyspa community.

この記事はPyspa Advent Calendar 2015のエントリとして書かれました。次の記事はこちらです。

識別子に絵文字などを使えるようにする

とある言語で識別子に絵文字などのシンボルが使えるそうなのですが、一応GoだってUnicodeクリーンなんだ。

f:id:moriyoshi:20140603121552p:plain

これをコンパイラに食わせてみる。

$ go run sushi.go

どうだ?

# command-line-arguments
./sushi.go:3: invalid identifier character U+1f363
./sushi.go:8: invalid identifier character U+1f363

なのでちょっと悔しい。ということでやりました。


$ go run sushi.go
sushi~

「:」を引数の名前と型の間に入れられるようしたりにする魔改造

とある言語が

func halfOpenRangeLength(start: Int, end: Int) -> Int {
    return end - start
}
println(halfOpenRangeLength(1, 10))
// prints "9"

のような文法だったのを見てウッと思ったのでやってみた

Go のソースを落としてきて

を当てて

$ (cd src/cmd/gc && rm y.tab.h && make)

とする。

package main

type Int int

func halfOpenRangeLength(start: Int, end: Int) -> Int {
	return end - start
}

func main() {
	println(halfOpenRangeLength(1, 10))
	// prints "9"
}
$ go run test.go
9

どーん