Julia - Jupyter notebook と Julia のソースコードの相互変換方法

Jupyter notebook って便利ですよね。私も数式の入ったメモを入れたい場合や、コードとプロット結果を一緒にしたいときにはよく利用しています*1

ただ一つ問題があって git との相性が恐ろしいほどに悪いことで有名ですよね。たった1行コードを追加しただけでも notebook の内部はそれ以上に変化してしまうので、git で管理しようにもどこに変更が入ったのか全然わかりません。

折衷案として notebook をソースコードに変換してそれを git で管理すると言う方法があります。

notebook のままだと1行追加するだけでも jupyter を起動せねばならず私としては億劫なのですが*2ソースコードなら編集も気軽にすることができます。しかし、nbconvert では notebook --> ソースコードの変換はできても、ソースコード --> notebook の変換ができないことが悩みの種でした。

そんなところに彗星のごとく現れた救世主が今回紹介する Literate.jl です。これを使うことで Julia のソースコードから notebook を生成することができます。README を読んだ時はいまいちイメージできなかったのですが、実際に notebook が作成されると感動しました!

github.com

以下、$ で始まるコードブロックは bash での実行を、julia> で始まるコードブロックは Julia の REPL での実行を表します。

目次

環境

julia> versioninfo()
Julia Version 1.0.0
Commit 5d4eaca0c9 (2018-08-08 20:58 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Xeon(R) CPU           E5645  @ 2.40GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, westmere)
Environment:
  JULIA_SHELL = /usr/bin/zsh

インストール

Literate.jl をインストール

julia> using Pkg; Pkg.add("Literate")

notebook からソースコードを生成する時に jupyter_contrib_nbextensions も必要らしいので入れます。

$ conda install -c conda-forge jupyter_contrib_nbextensions

github.com

使い方

Julia のソースコードを notebook に変換

まずはサンプルとして以下の内容を example.jl として作ってください。

# # Hello World
# こんにちは

x = 1
println("Hello World!")

ソースコード上でコメントになっているところが markdown cell になり、コードの部分が code cell になります。

Julia を起動し、以下のコマンドを実行してみましょう。

julia> using Literate

julia> Literate.notebook("example.jl", "."; documenter=false, credit=false, execute=false)

すると、example.ipynb という notebook がカレントディレクトリにできるはずです。 中身を見てみましょう!

f:id:goropikarikun:20180902223710p:plain

どうですか!ちゃんと notebook が生成されましたよ!!!

先程は executefalse にしましたが、ここを true にすると実行までされた notebook が生成されます。

julia> Literate.notebook("example.jl", "."; documenter=false, credit=false, execute=true)

f:id:goropikarikun:20180902223851p:plain

#- を入れると cell を区切ることが出来ます。

example.jl

# # Hello World
# こんにちは

x = 1
#-
println("Hello World!")

f:id:goropikarikun:20180902223919p:plain

Julia のソースコードmarkdown に変換

LIterate 使うと Julia のソースコードから markdown の生成も出来ます。

Literate.markdown("example.jl", "."; documenter=false, credit=false)

f:id:goropikarikun:20180902223911p:plain

notebook を Julia のソースコードに変換

心を新たにし、example.jl はまだ無いとします。
notebook から Julia のソースコードへの変換は "Download as" で Julia を選べばOK♪...では無いんです。残念。。。

f:id:goropikarikun:20180902233358p:plain

実際ダウンロードしてみればわかりますが、下記のように markdown の内容が消失します。

x = 1

println("Hello World!")

Python をダウンロードする時はちゃんと markdown も残ってるんですけど、Julia の場合だと消失します。 IJulia にあった issue によると nbconvert の問題だそうです。

github.com

うまいことテンプレートを作ると良いらしいのですが、ドキュメントを読んでもよくわからなかったので、上のリンク中で Carreau さんが提示されているテンプレートを使わせていただきました。
Customizing nbconvert — nbconvert 5.3.1 documentation

julia.tplを新規作成

{%- extends 'script.tpl' -%}

{% block markdowncell scoped %}
{{ cell.source | comment_lines(prefix='# ') }}
{% endblock markdowncell %}

notebook を Julia のソースコードに変換

$ jupyter nbconvert --to script 'example.ipynb' --template=julia.tpl

すると example.jl というファイルが出来ます。中身はこんな感じ。

# # Hello World
# こんにちは

x = 1

println("Hello World!")

変換されたこの書式はまさに Literate.jl で notebook に変換できる書式そのものではありませんか!!!

問題点

notebook --> ソースコードソースコード --> notebook と相互変換が出来るようになりましたが、完全に可逆というわけではありません。

ソースコード --> notebook の変換の時 #- を入れると cell を分割することが出来ましたが、notebook --> ソースコード では分割を表すような記号が入らないので notebook --> ソースコード --> notebook と変換をすると cell の分割が消えます。テンプレートで回避できそうな気がしますが、私は Jupyter の構造に詳しくないので解決策はわかりません。

おわりに

Literate で変換するソースコードを git で管理すれば、間接的に notebook を git で管理していることになるので大分 notebook のバージョン管理がやりやすくなるのではと思います。

Python だと gitnb というのを使うと notebook を git で管理しやすいようです。
GitHub - brookisme/gitnb: git tracking for python notebooks

*1:パッケージ作るときはもっぱら Atom 使っています

*2:エディタで直接編集することもできますが、編集箇所を探すのが大変

Julia - 関数に型そのものを渡す

環境

  • OS: ArchLinux
  • Julia 1.0.2

型そのものを関数に渡す

整数型の変数とかではなく Int とか型そのものを引数に取る関数を作りたい場合、

hoge(::Type{Int}) = println("Hello World!")

の様に引数にとりたい型を Type でくるんでやればいいらしい。

julia> hoge(::Type{Int}) = println("Hello World!")
hoge (generic function with 1 method)

julia> hoge(Int)
Hello World!

julia> hoge(Float64)
ERROR: MethodError: no method matching hoge(::Type{Float64})
Closest candidates are:
  hoge(::Type{Int64}) at REPL[1]:1
Stacktrace:
 [1] top-level scope at none:0

Julia を使い始めて以来、pretty-print (Base.show(io::IO, ::MIME"text/plain", z::Polar{T}) とか)に出てくるMIMEのところは仮引数がないけど何を渡しているんだと思ってきたのですが、型を渡しているのだと考えると色々と合点がいく気がします。

Julia のドキュメント中に型そのものを渡す方法の記述が見つからない*1ので保証はしませんが。randソースコードを見ると合ってる気がするようなしないような。

参考

*1:Type で検索するとノイズが多すぎてあるのかどうかわからない

Julia - パッケージ作り

目次

TL;DR

  • 野良パッケージはどんどん作ろう!
  • 新規の METADATA デビューは今は待ったほうが良いかなぁ。。。

環境

Julia Version 1.0.0
Commit 5d4eaca0c9 (2018-08-08 20:58 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Core(TM) i5-4460T CPU @ 1.90GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, haswell)
Environment:
  JULIA_SHELL = /bin/bash
  JULIA_EDITOR = nvim

自作のパッケージ

オリジナルパッケージの雛形を作る

2019/1/29 追記: 以下の方法でも雛形作れますが、現在は PkgTemplates.jl を使えばライセンスファイルや TravisCI の設定ファイルを一度に作ることができるので最終的に GitHub に公開するならばこちらを使ったほうが後々の手間が省けます。

github.com

(v1.0) pkg> generate MyPackage # 自作のパッケージの雛形を作成
Generating project MyPackage:
    MyPackage/Project.toml
    MyPackage/src/MyPackage.jl

shell> cd MyPackage/
/home/arch/tmp/MyPackage

(v1.0) pkg> activate .

(MyPackage) pkg> 

自動的に出来るファイルの中身
src/MyPackage.jl

module MyPackage

greet() = print("Hello World!")

end # module

この module の間にずらずらプログラムを書いていくと晴れて自分のパッケージが完成します。 詳しくは公式ドキュメント https://docs.julialang.org/en/stable/manual/modules/ を読んでください。

試しに作ったパッケージを読み込んでみます。

julia> using MyPackage
[ Info: Precompiling MyPackage [f41f8282-a16f-11e8-2120-1f4619698387]

julia> MyPackage.greet()
Hello World!

関数を追加する

src/MyPackage.jl

module MyPackage

greet() = print("Hello World!")
hello(io::IO, name) = print(io, "Hello ", name)
hello(name) = hello(stdout, name)

end # module

Julia を立ち上げ直して、もう一度パッケージを読みこみ直す。

(v1.0) pkg> activate .

(MyPackage) pkg> 

julia> using MyPackage
[ Info: Recompiling stale cache file /home/arch/.julia/compiled/v1.0/MyPackage/o5PoH.ji for MyPackage [f41f8282-a16f-11e8-2120-1f4619698387]

julia> MyPackage.greet()
Hello World!
julia> MyPackage.hello("Mike")
Hello Mike

テストを書く

テストケースは test/runtests.jl に書いていきます。@test 後が false になるとテストを通すのに失敗します。

using Test, MyPackage

@test sprint(MyPackage.hello, "Mike") == "Hello Mike"

Project.toml を編集。テストだけに使うパッケージは [extras][targets] に書きます。

name = "MyPackage"
uuid = "f41f8282-a16f-11e8-2120-1f4619698387"
authors = ["goropikari <goropikari56@gmail.com>"]
version = "0.1.0"


[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]

現在のディレクトリ構成はこんな感じ

shell> tree
.
├── Project.toml
├── src
│   └── MyPackage.jl
└── test
    └── runtests.jl

2 directories, 3 files

テストする。 tests passed と出れば OK。

(MyPackage) pkg> test
   Testing MyPackage
 Resolving package versions...
   Testing MyPackage tests passed

自分のパッケージの中で他のパッケージを使う場合

自分のパッケージの中で Random モジュールを使う例を通して v0.6 までとの違いを紹介します。 例えば、以下のようにプログラムを書いたとします。

MyPackage.jl

module MyPackage

using Random

greet() = print("Hello World!")
hello(io::IO, name) = print(io, "Hello ", name)
hello(name) = hello(stdout, name)
randstr() = randstring()

end # module

Julia v0.6 までは、今回の Random module のように Julia に標準実装された module に関しては REQUIRE *1 に書かずとも自分のパッケージ内で using してもその module を普通に利用することができました。

しかし、Julia v1.0 からは例え標準モジュールであろうとも using または import で読み込む module は Project.toml に書かなければならなくなりました。なかなか面倒な仕様になったのですが、幸いこれらを自分で Project.toml に書く必要はありません。add でこれらの module を追加すると自動的に Project.toml が更新されます。

(MyPackage) pkg> add Random
  Updating registry at `~/.julia/registries/General`
  Updating git-repo `https://github.com/JuliaRegistries/General.git`
 Resolving package versions...
  Updating `Project.toml`
  [9a3f8284] + Random 
  Updating `Manifest.toml`
 [no changes]

Project.toml が自動的に更新される。

name = "MyPackage"
uuid = "f41f8282-a16f-11e8-2120-1f4619698387"
authors = ["goropikari <goropikari56@gmail.com>"]
version = "0.1.0"

[deps]
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]

ついでに Manifest.toml というのが出来きますが、このファイルにはテストしたときの全てのパッケージ情報が書かれていています。Project.tomlManifest.tomlという2つの軽量なテキストファイルを人に渡すだけで、自分がテスト通したときの環境がそっくりそのまま他の人のマシン上でも再現できるので、再現性が重要になってくる分野では便利でしょとのこと*2

関数を追加したのでテストケースも追加します。

using Test, MyPackage, Random

Random.seed!(0)

@test sprint(MyPackage.hello, "Mike") == "Hello Mike"
@test MyPackage.randstr() == "0IPrGg0J"

もう一度テスト

(MyPackage) pkg> test
   Testing MyPackage
 Resolving package versions...
   Testing MyPackage tests passed 

Julia 以外のライブラリに依存する場合

既存のC*3のライブラリを使いたい、Julia だと処理が遅い部分を C で書いたコードを使いたいといった場合、deps/build.jl を作って build の仕方を書いていきます。 一番手っ取り早い方法だと Makefile 作って build.jl 内に run(`make`) とか書けばいいのですけど、C のコンパイラ入れていない人もいるでしょうし、そもそも make も使えるのか等々あるのでそんなに話は単純ではありません。

この辺のことについて私は詳しくないので以下を参考にしてください。 おそらく、公式ドキュメントには載っていません。

julialang.github.io

github.com

github.com

github.com

UUID

今までスルーしてきましたが Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" などの謎の文字列は各パッケージのUUIDです。 Generalを見れば調べられますが*4、探すの面倒なので上記の ]add Package 使って入れていくのが吉。ただ、add で入れると全部 [deps] のほうに入ってしまうのでテストだけにしか使わないものは適宜 [extras] の方に自分で移し [targets] も編集します。

GitHub に公開する

作ったパッケージを野良パッケージ*5として GitHub に公開してみましょう!

まずは GitHub に新しくリポジトリを作ります。Julia のパッケージの慣例としてパッケージ名の最後に .jl をつけるので今回は MyPackage.jl とします。(慣例で .jl と名前をつけますが、これをつけておくと確か野良パッケージでも Julia Observer に見つけてもらえます。)

作ったら以下を実行。もちろん GitHub のアカウントはご自身のものに直してください。

$ cd /path/to/MyPackage
$ git init
$ git add -A
$ git commit -m 'first commit'
$ git remote add origin https://github.com/goropikari/MyPackage.jl
$ git push -u origin master

これで野良パッケージの公開終了。今後は以下のようにして誰でもインストールできるようになります。

(v1.0) pkg> add https://github.com/goropikari/MyPackage.jl

適宜ライセンスやREADMEも加えましょう。

MyPackage に PATH を通す

作ったパッケージはディレクトリを activate していると using などで読み込めますが、そうでない一般の時は PATH が通っていないので読み込むことが出来ません。

julia> using MyPackage
ERROR: ArgumentError: Package MyPackage not found in current path:
- Run `Pkg.add("MyPackage")` to install the MyPackage package.

Stacktrace:
 [1] require(::Module, ::Symbol) at ./loading.jl:817

バリバリ開発しているときは activate すればよいですが、一段落付いて普通のパッケージとして使いたい時は下記のようにして PATH を通します。

julia> push!(LOAD_PATH, "/path/to/MyPackage/src")

こうすると他のパッケージ同様、どこのディレクトリにいても読み込むことが出来ます。

julia> using MyPackage

julia> MyPackage.greet()
Hello World!

ただこのままだと 毎度、毎度 PATH を通さなければならないので ~/.julia/config/startup.jl に追記しておきましょう。

push!(LOAD_PATH, "/path/to/MyPackage/src")

METADATA.jl に登録する

作ったパッケージを Julia の公式パッケージとして登録*6したい場合は Travis CI を通すことが必須です。 PythonPyPI ではコードレビューが無いらしい*7ですが Julia の場合はあるので、print("Hello World") しかないようなパッケージを登録しようとすると却下されます。

README またはドキュメントもちゃんと書きましょう。当たり前ですが英語で*8。 ドキュメントが書かれていないパッケージはいかに優れていたものだったとしても使う気になりません。

Julia v0.6 まではパッケージの命名に関する決まりごと*9等もドキュメントに書いてありましたが、現時点 (2018/8/17) の Julia v1.0 のドキュメントではパッケージの登録方法の項目が見当たらないので、どういうルールになったのかはよくわかりません。概ね一緒だと思いますが。

登録をするときは AttoBot を使うことが推奨されていますが、現状これを使う場合は v0.6 の作法の REQUIRE というファイルを作らなければなりません。Pkg3用に attobot3 というのもあるようですが、従来の attobot と同様にして使えるのかは不明。

パッケージの雛形を作ってくれる PkgDev.jl*10 がまだ Julia v1.0 に対応しておらず、また、Julia v1.0 が急にリリースされたせいで以前から Julia のパッケージを作っていた古参 Julian も困惑している感じが否めないので新規の METADATA デビュー *11 は当面控えたほうがよさそうといった印象です。

それでも「いますぐ私の素晴らしいパッケージを世に広めたいんだ!」という方は Julia Discourse や Julia の Slack であたりで質問してください。私は従来の方式でしかパッケージ公開をしたことないのでアドバイスできるようなノウハウがありません。

discourse.julialang.org

続編書きました。 goropikari.hatenablog.com

参考

LICENSE

Copyright (c) 2018: goropikari

License: MIT https://opensource.org/licenses/MIT

Copyright (c) 2009-2018: Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors:

https://julialang.org/license

*1:現在のProject.tomlに相当

*2:https://youtu.be/GBi__3nF-rM

*3:C言語でなくても構いませんが

*4:標準モジュールについてはありませんが

*5:METADATA.jl に登録されていないパッケージ

*6: add PackageName でインストールできるようになる

*7:PyPI 使ったことがないので確証はありませんが

*8:今Julia使っている人の英語力なら特に問題ないはず

*9:https://docs.julialang.org/en/v0.6.4/manual/packages/#Creating-a-new-Package-1

*10:Travis とかのテンプレも作ってくれる

*11:今後は General デビューというべきなのかもしれない

Juia - print 入門 - Hello World から pretty-print まで -

この記事は Qiita に 2018/08/04 に投稿したものです。2020/3/26 移行しました。

目次

print や類似の関数の基本的な使い方から、一緒に使うと便利な関数について紹介したいと思います。 今回はREPLの表示をそれなりにきれいにすることを目標とします。 人の美への追求は際限なく 「IJulia 使えば Markdown も使えるからキレイに出力できる!」と考えるかもしれませんが、掛けた労力に対して得られる成果があまりに微々たるものなので今回は扱いません。1つの数字を MathJax で表示するくらいなら良いのですが配列が絡んでくると地獄です。

例えば "量子力学と言えば Dirac 表記でしょ!" といった軽いノリで 下記のように表示されていたものを

 0.7071067811865475
 0.0               
 0.0               
 0.7071067811865475

MathJaxを使って下記のように表示させようとするのはおすすめしません。地獄を見ます。 $$0.707 | 00 \rangle + 0.707 | 11 \rangle$$

たかが1行の出力のために何故600行以上もコードを書いているんだと虚無感が襲ってきます。

できるようになること

今まで↓のように表示させていたものが

Name Age Point
Carmen.Blanco 21 11.7303
Corrales.Cristobal 21 1.5121000000000002
Wendolin.Urías 30 1.1905999999999999
Nazario.Roberto 43 2.7626
zSantacruz 47 6.9193999999999996
Sisneros.Emilio 30 2.5084
Delia.Galarza 31 19.1949
Collado.Tomás 43 10.206900000000001
Abraham80 50 7.512499999999999
wAlonso 24 4.7661

↓のように表示させることができるようになります。

Name                   Age    Point     
Carmen.Blanco          21     11.7303
Corrales.Cristobal     21      1.5121
Wendolin.Urías         30      1.1906
Nazario.Roberto        43      2.7626
zSantacruz             47      6.9194
Sisneros.Emilio        30      2.5084
Delia.Galarza          31     19.1949
Collado.Tomás          43     10.2069
Abraham80              50      7.5125
wAlonso                24      4.7661


自作の型の表示が↓から

Horse("ハリボテエレジー", "手作好太郎", "ダンボウルガクエン", "ガムテイプマツリ", 6, [8, 8, 8, 8, 8, 8, 8, 8, 8, 8  …  8, 8, 8, 8, 8, 8, 8, 8, 8, 8])

↓になります。

Horse
 競走馬名: ハリボテエレジー
 騎手    : 手作好太郎
 父馬    : ダンボウルガクエン
 母馬    : ガムテイプマツリ
 馬齢    : 6
 着順
 8
 8
 8
 ⋮
 8
 8

一見すると簡単に出来そうですが、この記事の長さが物語っているようにこの程度の出力を得るだけでもすごーく面倒です。

環境

julia> versioninfo()
Julia Version 1.0.0
Commit 5d4eaca0c9 (2018-08-08 20:58 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Core(TM) i5-4460T CPU @ 1.90GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, haswell)
Environment:
  JULIA_SHELL = /usr/bin/zsh
  JULIA_EDITOR = nvim

今回使うJulia は安定版の v1.0 です。Julia v0.6 とは文法がほとんど違うので注意!

基本

他のプログラミング言語よろしく、print を使うと文字列や変数を画面に出力することができます。 ここで println を使うと最後に改行が入ります。

julia> print("Hello World")
Hello World
julia> println("Hello World"); println("Hello goropikari")
Hello World
Hello goropikari

各変数をカンマ区切りで渡すと連結されて出力されます。

julia> print("Hello", 1, "World")
Hello1World

printと似たもので show もありますが、こちらは print よりも変数について時として詳しく表示されます。

julia> x = 10
10

julia> println(x)
10

julia> show(x)
10
julia> x = Float16(1)
Float16(1.0)

julia> println(x)
1.0

julia> show(x)
Float16(1.0)

printshow はちょいちょい表示のされ方が違うので要注意。

julia> print("Hello\n")
Hello

julia> show("Hello\n")
"Hello\n"


出力先を明示的に指定しなかった場合、結果は標準出力に出力されます。出力先を指定したい場合は print の第一引数に出力先を指定します。

# 標準出力
julia> println(stdout, "Hello World")
Hello World

# 標準エラー出力
julia> println(stderr, "Hello World")
Hello World


JuliaにはC言語スタイルで print するためのマクロ @printf が用意されています。 これを使うとC言語と同じ様に print することができます。

julia> using Printf

julia> @printf("Hello%010d", 12345)
Hello0000012345


print@printf に似たものに sprint@sprintf があります。 違いは何かと言えば頭に s がついているほうは print したものを画面に表示するのではなく文字列として返します。

julia> using Printf

julia> @sprintf("Hello%04d", 3)
"Hello0003"

julia> @sprintf("Hello%04d", 3); # 標準出力に出しているわけではないので 
                                 # ; をつけると何も表示されない

@sprintf の方は比較的直感的です。一方で sprint の方はどうでしょう?

julia> sprint("Hello")
ERROR: MethodError: no method matching sprint(::String)
Closest candidates are:
  sprint(::Function, ::Any...; context, sizehint) at strings/io.jl:97
Stacktrace:
 [1] top-level scope at none:0

ありゃま、エラーが出ました。@sprintf と違い sprint には第一引数に関数を与えなければなりません。また、与える関数は第一引数に IO を受け取れるようなものに限ります。例えば printshow など。

julia> sprint(print, "Hello")
"Hello"

julia> sprint(print, 12345)
"12345"

IOContext

凝った出力をするため、また、出力結果を期待通りにするために IOContext の理解は避けては通れないのでここで紹介します。 IOContext は簡単に言えばどこどう出力するかをまとめたものです。

"どこ" には標準出力や標準エラー出力、書き込み可能なファイルなど。 "どう" はコンパクトに表示する、省略して表示する、色をつけて表示するなどなどです。

基本文法は

IOContext(io::IO, key1 => value1, key2 => value2, ...)

です。ここで key の部分は自分で決めることができますが、以下のよく使われているものに関しては同じ名前をつけると不具合が出る可能性があるので他の名前をつけましょう。

  • :compact 値をコンパクトに表示するか否か
  • :limit 表示を省略するか否か。Juliaでは配列は省略されて表示されますがそれはこれが true になっているから。
  • :displaysize 表示部分のサイズ。
  • :color 出力の文字に色をつけるか否か
  • :typeinfo 私は使ったことがないのでよくわかりません。すいません。show を使ったときには表示が変わるなぁということだけ確認しました。
julia> show(Float16(1))
Float16(1.0)
julia> show(IOContext(stdout, :typeinfo => Float16), Float16(1))
1.0

例えば、コンパクトに表示させたときとそうでないときの差は以下のとおりです。

julia> using Random; Random.seed!(2018); x = rand()
0.6545394330653942

julia> io = IOContext(stdout, :compact => true)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> println(io, x)
0.654539

julia> io = IOContext(stdout, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> println(io, x)
0.6545394330653942

IO が特定の key を持っているか否かを調べる時は haskeyを使います。 key を持っていたらその値を、持ってなかったら指定したの値を返すようにするには get を使います。

julia> io = IOContext(stdout, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> haskey(io, :compact)
true

julia> haskey(io, :hoge)
false

julia> get(io, :compact, true)
false

julia> get(io, :hoge, true)
true

普段遣いのときには IOContext はあまり使わないと思いますが、自分のパッケージを作って後述の pretty-print をするときは結構重要になってきます。 特に変数の値でなく、表示のされ方をテストしたいときは IOContext を知らないと、ちゃんとしたテストケースが書けません。

出力をきれいに魅せる

カラフルにする

printstyled を使うと色付きだったりボールド体で出力することができます。 色の指定方法は色の名前を指定するか、0〜255までの整数値で決めることができます。

julia> printstyled("Hello\n", color=:cyan)
julia> printstyled("Hello\n", color=:reverse) # 反転
julia> printstyled("Hello\n", color=:underline) # アンダーラインを引く
julia> printstyled("Hello\n", color=:light_blue, bold=true) # Julia v0.5 を使ったことがある人には懐かしのスタイル

f:id:goropikarikun:20181110181922p:plain

文字幅

数字を普通に出力すると以下の様に左寄せに出力されますが、時として位の位置を揃えて出力したいと思いますよね。

julia> for i in [1, 10, 100, 1000]
           println(i)
       end
1
10
100
1000

このように。

   1
  10
 100
1000

第一の方法は @printf を使う方法です。

julia> for i in [1, 10, 100, 1000]
           @printf("%4d\n", i)
       end
   1
  10
 100
1000

しかし私はC言語風の書き方に慣れていないので @printf は使いづらいです。 変数の数が増えてくるとどこがどこに対応しているのかパット見だとわからないので好きになれません。そのため私は lpad を使っています。

julia> for i in [1, 10, 100, 1000]
           println(lpad(i, 4))
       end
   1
  10
 100
1000

lpad は指定した文字数 $n$ よりも文字列 $s$ が短かったら、足りない分だけ $s$ の左に空白を入れた文字列 $s'$ を返します。右に空白を入れる場合は rpad を使います。 第3引数も指定すると空白以外の文字で埋めることができます。0埋めの連番ファイルを作りたいときなどにも便利です。

julia> lpad("Hello", 10)
"     Hello"

julia> lpad("Hello", 10, "-")
"-----Hello"

julia> lpad(1, 10, "hoge")
"hogehogeh1"

julia> rpad("Hello", 20)
"Hello               "

julia> rpad("Hello", 20, "-")
"Hello---------------"

julia> lpad(1, 6, "0")
"000001"

小数・複素数の表示をきれいにする Base.alignment

表示させたいものが整数ならば lpad を使えば位を揃えて表示することが出来ました。次は小数や複素数をきれいに表示させてみましょう。

配列を作った時、小数だったら位の位置を揃えて、複素数だったら実部と虚部を結ぶ符号の位置が揃って表示されますが、それを自力で実現するための方法を紹介します。

julia> x = randn(3) * 100
3-element Array{Float64,1}:
  -99.60800864461076 
   28.202047839771822
 -158.385848352147

julia> randn(ComplexF64, 3) * 100
3-element Array{Complex{Float64},1}:
 -11.13643420355874 + 21.75267947789508im  
 41.913378452353996 - 56.76768974241606im  
  83.73402073531838 + 0.15488964757358356im

完全に自力でやろうとすると整数部が何桁で小数部が何桁、実部が何桁で虚部が何桁・・・とやらないといけないわけですが、幸いそれらをやってくれる関数があります。それが Base.alignment です。

ちなみに Base.alignment はマニアックな関数なので Julia の公式ドキュメントに使い方は書いてありません。あまりに誰も使わないせいか docstring も2年近く間違ったまま放置されています。それくらいマニアックです。

Base.alignment の基本文法

Base.alignment(io::IO, x)

Base.alignmentIO と数字を入れると要素数2のタプルが返ってきます。もし入れた数字が小数ならば第1要素は整数部の桁数、第2要素は点を含めた小数部の桁数になります。 printshow と違い、 Base.alignment の場合は IO を省略できません。何故かと言えば context (compact とか) によって同じ数字でも表示される桁数が違うからです。

julia> x = rand()
0.6022015583915965

julia> println(IOContext(stdout, :compact => true), x)
0.602202

julia> io = IOContext(stdout, :compact => true)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> Base.alignment(io, x)
(1, 7)

julia> println(IOContext(stdout, :compact => false), x)
0.6022015583915965

julia> io = IOContext(stdout, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> Base.alignment(io, x)
(1, 17)

それでは、実際に Base.alignment を使って小数をきれいに表示させてみましょう。 方針は配列の要素全てについて Base.alignment で調べ、整数部の桁数の最大値と小数部の桁数の最大値の和を文字幅とするように表示させます。整数部、小数部の足りない分の桁は repeat をつかって空白で埋めます。

julia> x = randn(5) * 10
5-element Array{Float64,1}:
  -9.212777882398562 
  -0.6891109738516901
  -2.6882962369764787
 -36.9082211306946   
   4.914965667105987 

julia> l = r = 0
0

julia> a= []
0-element Array{Any,1}

julia> io = IOContext(stdout, :compact => true)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> for i in x
          atmp = Base.alignment(io, i)
          global l = max(l, atmp[1])
          global r = max(r, atmp[2])
          push!(a, atmp)
       end

julia> for (al, item) in zip(a, x)
          ls = repeat(" ", l - al[1])
          rs = repeat(" ", r - al[2])
          println(io, ls, item, rs)
       end
 -9.21278 
 -0.689111
 -2.6883  
-36.9082  
  4.91497

1つ目の例を再現

ここまでで冒頭の1つ目の例を再現することができます。

julia> # using Pkg; Pkg.add("Faker")

julia> using Faker, Random

julia> Random.seed!(0);

julia> un = Faker.user_name
user_name (generic function with 1 method)

julia> age() = rand(20:50)
age (generic function with 1 method)

julia> point() = round(abs(randn()), digits=5)
point (generic function with 1 method)

julia> n = 10
10

julia> data = Matrix{Any}(undef,n,3);

julia> for i in 1:n
           data[i,1] = un()
           data[i,2] = age()
           data[i,3] = point() * 10
       end

julia> begin
           name = "Name"
           a = "Age"
           p = "Point"
           io = IOContext(stdout, :compact => true)
           wn = max(length(name), maximum(length.(data[:,1]))) + 5
           wa = 7
           l = r = 0
           for i in 1:n
               aij = Base.alignment(io, data[i,3])
               global l = max(l, aij[1])
               global r = max(r, aij[2])
           end
           println( rpad(name, wn) * rpad(a, wa) * rpad(p, max(10,l+r)))
           for i in 1:10
               aij = Base.alignment(io, data[i,3])
               ls = repeat(" ", l - aij[1])
               rs = repeat(" ", r - aij[2])
               println(io, rpad(data[i,1], wn), rpad(data[i,2], wa), ls, data[i,3], rs)
           end
       end
Name                   Age    Point     
Valladares.Cynthia     43      4.7313
Rolando20              49      1.5648
Gabino16               45      4.4951
Gabino.Cepeda          25      9.6989
Isabela.Pulido         35     13.5565
Eloy25                 43     25.1382
KGuillen               35      5.1041
Sonia54                39     18.9942
Anabel.Mesa            32     28.9205
nGracia                50      6.3489

配列 Base.print_array

続いて配列の表示についてです。

配列を print してみた時、「思ってたのと違う」と感じたことはないでしょうか?

julia> x = rand(5)
5-element Array{Float64,1}:
 0.8564231118379442
 0.3957938354126007
 0.0803914002136299
 0.919405100907382 
 0.3769282818406279

julia> println(x)
[0.856423, 0.395794, 0.0803914, 0.919405, 0.376928]

julia> y = rand(5,5)
5×5 Array{Float64,2}:
 0.528446  0.948737  0.996994  0.862299  0.675343
 0.633248  0.637487  0.34968   0.767471  0.960373
 0.868802  0.253233  0.235227  0.709841  0.134395
 0.587811  0.637926  0.33784   0.455068  0.180991
 0.912758  0.550064  0.916355  0.58957   0.386093

julia> println(y)
[0.528446 0.948737 0.996994 0.862299 0.675343; 0.633248 0.637487 0.34968 0.767471 0.960373; 0.868802 0.253233 0.235227 0.709841 0.134395; 0.587811 0.637926 0.33784 0.455068 0.180991; 0.912758 0.550064 0.916355 0.58957 0.386093]

変数を定義した時に表示される方法で出力してほしいのであって、一行にずらずら要素を出力してほしいわけじゃないんだ!!!と思ったことはないでしょうか?

そんな時は show を使います。しかし、そのまま使うと print と大差ありません。 IOMIME を指定すれば望みの結果が得られます。 REPL のときの MIME は "text/plain" と覚えとけば、とりあえず良いと思います。

julia> println(x)
[0.856423, 0.395794, 0.0803914, 0.919405, 0.376928]

julia> show(x)
[0.856423, 0.395794, 0.0803914, 0.919405, 0.376928]
julia> io = IOContext(stdout, :limit => true, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> show(io, "text/plain", x)
5-element Array{Float64,1}:
 0.8564231118379442
 0.3957938354126007
 0.0803914002136299
 0.919405100907382 
 0.3769282818406279

さてさて、無事に望みの結果を得ることが出来ましたが、人によってはヘッダー部( 5-element Array{Float64,1}: など)を表示させたくないと望むことでしょう。そんなときは Base.print_array を使います。

ちなみに Base.alignment同様、Base.print_array も公式ドキュメントには使い方が載っていないマニアックな関数です。ただ、alignment と比べて実用性高いのでドキュメントに載せて良いのではと個人的には思います。

Base.print_array の基本文法

Base.print_array(io::IO, x)
julia> io = IOContext(stdout, :limit => true, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> Base.print_array(io, x)
 0.8564231118379442
 0.3957938354126007
 0.0803914002136299
 0.919405100907382 
 0.3769282818406279

julia> Base.print_array(io, rand(30))
 0.826103394472415   
 0.12252942018471757 
 0.2578897738095447  
 0.9962502442037657  
 0.7355190804809946  
 0.7585607760377631  
 0.9151773834286789  
 0.3929985249254522  
 0.6641753306579214  
 0.331754333274941970.7833300985202769  
 0.18328052862651867 
 0.23765181941856928 
 0.023377483952839784
 0.09753998271209197 
 0.5774230030542189  
 0.8859464218457049  
 0.6723385022029065  
 0.47453039665141294 
julia> io = IOContext(stdout, :limit => false, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> Base.print_array(io, rand(30))
 0.7545909477271988  
 0.04426425050940841 
 0.8374874234291312  
 0.1487924398894196  
 0.9042352827314581  
 0.6163873731935219  
 0.8876188540406658  
 0.941831168132194   
 0.5473667368995183  
 0.06389212057729132 
 0.31826618585326116 
 0.5844167820961697  
 0.2901193343171964  
 0.04053208788398743 
 0.12097218515312957 
 0.008122564658366693
 0.26497805735815083 
 0.6564738355733903  
 0.058178777741022536
 0.3444346924786441  
 0.3245624908138902  
 0.8014413904453013  
 0.9545200342276534  
 0.154475460973849   
 0.7799280363802161  
 0.5400078688551904  
 0.8594465295979896  
 0.881281761468725   
 0.8220828940691287  
 0.9481617822907571 

~~ここでは扱いませんが Markdown で配列を表示させたい人は Base.print_matrix_row の使い方を覚えると良いかもしれません。*1

pretty-print

最後に pretty-print です。pretty-printの正確な定義がよくわかりませんが、垢抜けない野暮ったい出力をプリティーにすることでしょう(多分)。

下図の「ここ」とくくった部分をなんて呼ぶのか、または、「ここ」の部分を表示させる機能をなんて呼ぶのかわからないので説明しづらいのですが、「ここ」とくくった部分が何故このように表示されるかといったら、それはもちろんそのように定義されているからです。

f:id:goropikarikun:20181110182037p:plain

そしてこの「ここ」の部分の表示のさせ方は型によって特徴付けられています。

この「ここ」の部分の表示方法を変えたい場合は Base.show のメソッドを上書きすることで実現できます。

基本的な書き方

function Base.show(io::IO,  ::MIME"text/plain", x::MyType) # MyType の部分を好みの型に変える
    println(io, "A is ", x.a) # print に io を入れることを忘れないように!
    println(io, "B is ", x.b)
    ...
end

2つ目の例を再現

新たに作った Horse 型の表示方法を定義してみます。

julia> struct Horse
           name::String
           jockey::String
           sire::String
           dam::String
           age::Int
           finpos::Vector{Int}
       end

julia> x = Horse("ハリボテエレジー", "手作好太郎", "ダンボウルガクエン", "ガムテイプマツリ", 6, fill(8,100))
Horse("ハリボテエレジー", "手作好太郎", "ダンボウルガクエン", "ガムテイプマツリ", 6, [8, 8, 8, 8, 8, 8, 8, 8, 8, 88, 8, 8, 8, 8, 8, 8, 8, 8, 8])

julia> function Base.show(io::IO, ::MIME"text/plain", x::Horse)
           summary(io, x)
           println(io)
           println(io, " 競走馬名: ", x.name)
           println(io, " 騎手    : ", x.jockey)
           println(io, " 父馬    : ", x.sire)
           println(io, " 母馬    : ", x.dam)
           println(io, " 馬齢    : ", x.age)
           println(io, " 着順")
           Base.print_array(io, x.finpos)
       end

julia> x
Horse
 競走馬名: ハリボテエレジー
 騎手    : 手作好太郎
 父馬    : ダンボウルガクエン
 母馬    : ガムテイプマツリ
 馬齢    : 6
 着順
 8
 8
 8
 8
 8
 8
 8
 8
 8
 88
 8
 8
 8
 8
 8
 8
 8
 8

MIME を "text/markdown" で定義すると、 Jupyter Notebook を使っている場合 "text/markdown" で定義した方法が優先されます。 私がやるとしたら下記のように配列を含んでいなかったら Markdown への対応を考えるかもしれません。

function Base.show(io::IO, ::MIME"text/markdown", x::Number)
    println(io, "\$", x, "\$")
end
rand()

f:id:goropikarikun:20181110182114p:plain

配列を含んでいるとどう面倒なのかは下記の例を見ていただくと納得いただけるのではないかと思います。以下は先程の例を "text/markdown" に変えただけです。

function Base.show(io::IO, ::MIME"text/markdown", x::Horse)
   summary(io, x)
   println(io)
   println(io, " 競走馬名: ", x.name)
   println(io, " 騎手    : ", x.jockey)
   println(io, " 父馬    : ", x.sire)
   println(io, " 母馬    : ", x.dam)
   println(io, " 馬齢    : ", x.age)
   println(io, " 着順")
   Base.print_array(io, x.finpos)
end
x

f:id:goropikarikun:20181110182146p:plain

まとめ

  • 凝りだすと際限がないので程々のところでやめましょう。
  • print を凝りたい場合、公式ドキュメントは非力なので Julia のソースコードを読んでください。

参考

*1:Base.print_matrix をいじれば Markdown にすぐに対応できそうな気がしたのですがうまく行きませんでした。

安価な静電容量無接点方式キーボード NiZ Plum 75 を買ってみた

購入まで

私は普段キーボードは HHKB Lite2 と Mistel BAROCCO MD600 (赤軸) を使っています。 MD600 は今年の4月より使い始めましたが、最近は MD600 の押下圧に慣れてきたせいで HHKB の押下圧がすごく重く感じるようになってきました*1。 そのせいで HHKB Lite2 で長時間タイピングをしていると手が痛くて痛くてしょうがない。

PFU Happy Hacking Keyboard Lite2 英語配列 USBキーボード ブラック PD-KB200B/U

PFU Happy Hacking Keyboard Lite2 英語配列 USBキーボード ブラック PD-KB200B/U

気持ちとしてはもう一つ MD600 が欲しいのですが 1.5万円くらいするので気安く買おうとも思えません。 だからといって HHKB Lite2 の重さは結構なストレス。

どうしたものかと考えているある日、NiZ*2 というところが1万円ちょっとで静電容量無接点方式のキーボードを売っていることを知りました。 Amazonのレビューを信じる限り値段の割にはなかなか良いものらしい。

静電容量無接点方式といえば HHKB Professional や東プレ RealForce などのハイエンドモデルに採用されている構造で有名ですが、それが1万円ちょっとで買えるというのだから興味がそそられました。

私の理想としては

という条件を満たしたキーボード*3があったら嬉しいのですが、いかんせんそのようなキーボードは見たことも聞いたことも無い。

そんなこんなで MD600 をもう一個買うか、はたまた NiZ Plum 75キー*4 を買うかと迷っているときに NiZ Plum 75キー がAmazonタイムセールで8,479円で売っているのを発見。 しかも Keepa によるとこの1年間での最安値!!!さらにキャンペーンで666円引きだったので7,813円に。
いくら中華キーボードとは言え新品の静電容量無接点方式のキーボードが8000円切る値段で買えるなんて!!!

タイムセール終了まで残り時間が少なかったこともあり勢いで購入してしまいました。

f:id:goropikarikun:20180725001505j:plain HHKB Lite2 との比較。久々に独立したファンクションキーがあるキーボードを触った。

f:id:goropikarikun:20180725001514j:plain Caps Lock が有効になっていると緑色に光る。私が買ったモデルはRGBではないが何箇所か同様にバックライトがある模様。だが光る条件とどこが光るのかはよくわからない。

打ち心地

私は静電容量無接点方式のキーボードを初めて買いましたが、スコスコとした打ち心地がいいですね♪ HHKB Professional 等と比べると安っぽさは否めませんが、値段も半額以下と言うことを考慮すれば十分優秀です。

一番私が気にしていた押下圧について、私の感覚ではCherry 赤軸も相当軽いと思っていましたがそれを超える軽さです。ここまで軽いとは思いませんでした。今まで使ってきたHHKB Lite2 が重かったこともあり、指を置くだけで入力ができるかのようです。 まだ1日しか使っていませんがこちらの打ち心地が軽すぎて赤軸がすごく重く感じてしまうようになってしまいました。

ファームウェアのアップデート

キーボードの入っている箱にキーボードの取説とキー配置を変更するためのソフトをダウンロードするためのQRコードがついていますが、ソフトのバージョンが古かったのでメーカーのサイトからダウンロードしました。*5

Download – PLUM_NIZ keyboard

キー配置を変更

このキーボードはプログラマブルなのでキー配置を変えることができます。*6

キー配列を変えるためのソフトの使い方が最初よくわからなかったので、何回かタイプしても全く反応しなくなるという現象に陥りなかなか焦りました。

特に Fn の割当方法は他のキーの割当方法と違いわかりづらかったです。
f:id:goropikarikun:20180725020201p:plain
CapsLockをCtrlとかに割り当てる時はこちらを使う。しかし、Fn の場合押しても何にもならない。

f:id:goropikarikun:20180725020211p:plain
Fn を他のキーに割り当てたい場合、上のタブの Function を選び、その中に表示される FN マークのアイコンを選択。そうすると Fn キーの割当を他のキーにすることができる。

とりあえず私は日頃使っている MD600 と同じように設定しました。

私の設定

Before -> After

  • CapsLock -> Ctrl
  • Back Space -> \
  • \ -> `
  • 左ALT -> Fn
  • Fn -> Win
  • Fn + J -> ←
  • Fn + K -> ↓
  • Fn + L -> →
  • Fn + I -> ↑
  • Fn + ; -> Back Space
  • Fn + ' -> delete
  • Fn + H -> Home
  • Fn + N -> End

あとは全体的にアルファベットをDvorak配列と同じになるように割り振りました。
割り振るのは大変でしたが MD600と違ってカスタマイズしたキー配列の情報を .pro という拡張子のテキストファイルに保存できるので便利です。 デフォルトのProモードのキーレイアウトをDvorakに変更した(他のキーはそのまま)キーレイアウトファイルをGist に上げたのでDvorakに変更したい方はご自由にお使いください。
Dvorak layout for NiZ Plum 75

変な設定をしてしまいよくわからなくなっても、 最悪 ESC + Left Ctrl + Delete + Right Ctrl を5秒以上同時押しすれば Pro モードの配列が初期化されるので元にもどります。

私はこのキーボードを自宅で使うので Dvorak配列にしても誰に文句を言われることもありませんが、職場で使う場合はDvorak配列知らない人に打ってもらうことなどもあるかもしれません。そんなときはキーボード裏のスイッチでモードを Offce か Media に変更すれば、QWERTY になるので他の人に打ってもらう機会があっても問題ありません!

参考

Happy Hacking Keyboard | 高品位キー | PFU Plum 75 Keys 35g/45g Electro-Capacitive Keyboard Non RGB backlighting – PLUM_NIZ keyboard

追記

キーキャップを変えて無刻印化しました。 goropikari.hatenablog.com

*1:メーカーのデータによると HHKB Lite2 の押下圧は約 55 g、Cherry MX赤軸 は 約 45 g らしいので約 10 g の差。

*2:会社名なのかブランド名なのかよくわかりません。

*3:MD600が静電容量無接点方式になったら最高

*4:おそらく正式な名称はPlum 75 Keys 35g/45g Electro-Capacitive Keyboard Non RGB backlighting,

Plum 75 Keys 35g/45g Electro-Capacitive Keyboard Non RGB backlighting – PLUM_NIZ keyboard

*5:QRコードのリンク先のバージョンはv1.1.30. メーカーサイトに上がっているものは v1.1.35 だった。

*6:Windows