2021/12/20

golangの本を買いにいったが止めた

golang をネットで場当たり的に調べるのも良いが、本を買ってじっくり学ぶというのもやりたい。
私はまだ技術書をデジタル書籍ではうまく扱えないので、本屋さんに行ってきた。

紀伊國屋と丸善を回ったのだが、どちらも同じような状況だった。
買わずに家に帰って調べたのだが(買いに行く前にやれってね)、本屋さんにあったような本しかなかった。

 

入門書でも良いのだけど、インストールの仕方とかツールの使い方とかに比重が置かれているタイプではなく、サーバプログラミングに重点を置いているようなものでもなく、言語仕様に重点が置かれている本が買いたかったのだ。

となると、これしか出てこない。

プログラミング言語Go - 丸善出版 理工・医学・人文社会科学の専門書出版社
https://www.maruzen-publishing.co.jp/item/?book_no=295039

本屋さんにもあったのに買い控えたのは、これが2016年出版と5年前だからだ。オリジナルは2015年。
電子書籍版も2021年5月に出ているのだが、内容が更新されたわけではないだろう。
C言語だと多少古くてもなんとかなると思うのだが、Go言語は最近の言語。
6, 7年も前だと変更がいろいろ行われていそうで止めたのだ。

候補になりそうなのはオライリーさんのだけど、これはまだ日本語になってない。

Learning Go [Book]
https://www.oreilly.com/library/view/learning-go/9781492077206/

というわけで、買うのを止めたのだった。


しかし家に帰ってよく考えたのだが、ツールの使い勝手が変わっても言語自体は変わってないのかも・・・?
そこを調べておくべきだろう。

  • v1.1
    • integer division by zero
    • surrogates in Unicode literals
    • method values
    • return requirements
  • v1.2
    • use of nil
    • three-index slices
  • v1.3
    • 無しなのかな?
  • v1.4
    • for-range loops
    • method calls on **T
  • v1.5
    • map literals
  • v1.6
    • 無し
  • v1.7
    • terminating statements
  • v1.8
    • explicit converting
  • v1.9
    • type aliases
    • 浮動小数点演算の丸め関係
  • v1.10
    • 一応無し
  • v1.11
    • 無し
  • v1.12
    • 無し
  • v1.13
    • リテラルのプレフィクス追加(0b, 0o など)
    • ビットシフト量を unsigned に変更
  • v1.14
    • overlapping interfaces proposal
  • v1.15
    • 無し
  • v1.16
    • 無し
  • v1.17
    • conversions from slice to array pointer
    • unsafe.Add()
    • unsafe.Slice()
  • v1.18
    • これを書いている時点では未定
    • 404 は "to, err := human()" なのね

あまり読まずに抜き出したので、間違いがあったら済まぬ。

 

 

オリジナル本の "The Go Programming Lanuguage" は、どうも v1.5 の時代に書かれているようだ(Contents の PDFより)。

v1.6 以降で気になる変更点は、type alias のサポートとか interface関係かなぁ。
しかし標準ライブラリへの変更や追加もあるだろうから、やっぱり悩むな。。。

2021/12/19

[golang]go.mod の require にリリースバージョンを付けたい

以前、go.mod で特定の commit-id を指定するやり方を調べた。

hiro99ma blog: [golang] go.mod の pseudo-version
https://blog.hirokuma.work/2021/12/golang-gomod-pseudo-version.html

が、よく考えれば普通は pseudo じゃないバージョンを使っているのだ。
そのバージョンの指定方法を調べておくべきだろう。

https://go.dev/doc/modules/gomod-ref#require

require の文法は

require module-path module-version

で、リリースバージョンか pseudoバージョンとのこと。

リリースバージョンってなんじゃ?
git tag でよいのか?

前回の gogo-test4 に v0.1.0 のタグを付けて push。
GitHub のリリースにはしていないが、tags には出てくる。

https://github.com/hirokuma/gogo-test4/tags

で、これを require している gogo-test3 で指定する。

https://github.com/hirokuma/gogo-test3/blob/66a97f39163f91ab9bd945a1c45ff80de37a6d7d/go.mod#L5

go mod tidy してもエラーにならないから、これでよいようだ。

ただ、git tag って push した後でも消したり差し替えたりできると思うので安心しきってはいかんのかもしれんな。

[golang]ライブラリが持つグローバル変数

グローバル変数と呼んでよいのかどうかわからんが、パッケージ内に変数を置けばグローバル変数みたいな扱いができると思う。
小文字だとパッケージ外からはアクセスできないが、ローカル変数かグローバル変数かでいえばグローバル変数だからどこからアクセスしても本体は同じだ。

 

さて、ではこれがライブラリだったらどういう扱いになるのだろうか?
もし1箇所でしか使っていないなら気にしなくてよいと思うが、複数のパッケージで同じライブラリを import していたら、それは別のメモリとして扱われるのか、同じメモリとして扱われるのか。

https://github.com/hirokuma/gogo-test3

https://github.com/hirokuma/gogo-test4

gogo-test3 の中にパッケージ gogo1 と gogo2 があって、それぞれグローバル変数(パッケージ変数、の方が良いのか?)を置いている。
そしてそれぞれが gogo-test4 を import している。

gogo-test4 はグローバル変数がある。

gogo-test3 の main() で、gogo1 の中で gogo1 と gogo-test4 を更新 → gogo2 の中で gogo2 と gogo-test4 を更新 → gogo1 側の gogo-test4 の値を見てみる、ということをやっている。
結果はこうなる。 2つ数値が出力されるうち、前者が package 内のグローバル変数、後者が gogo-test4 のグローバル変数である。

$ go run .
gogo1 10, 10
gogo2 20, 20
gogo1 10, 20

gogo2 で設定した gogo-test4 の値が gogo1 の方でも取得されているので、取りあえずこういうシンプルな書き方であれば import を複数の箇所で行っていても 1つのものだと判定されるようだ。
New 関数を package が持っていることが多いのは、動的に変数を用意しないと知らないところからアクセスされて値が書き換わっていた、なんてことが起きてしまうのを防ぐためだ。
あるいは、どこからアクセスしても同じデータを使いたいなら、あえてそう作ればよいということだ。

2021/12/05

[golang] go.mod の pseudo-version

hiro99ma blog: [golang] go.mod の pseudo-version の調べ方がわからん
https://blog.hirokuma.work/2021/11/golang-gomod-pseudo-version.html

git log を加工したらよいのではなかろうか、ということでこうなった。

TZ=UTC git log -n1 --abbrev-commit --abbrev=12 --date=format-local:%Y%m%d%H%M%S

 

たとえばここだとこうなる。

$ TZ=UTC git log -n1  --abbrev-commit --abbrev=12 --date=format-local:%Y%m%d%H%M%S
commit 382e4677bbfb (HEAD -> main, tag: v3, origin/main, origin/HEAD)
Author: hirokuma <hiro9ma@gmail.com>
Date:   20211121014212

    v3

最初は時間がJSTになってて、go mod tidyするとタイムスタンプが違うといわれてしまった。
かといってどうやって UTC にするんじゃ?ってなって。TZ 設定したけど --date=format: にしていたので反映されずに悩んでしまった。

metadata - Git: show dates in UTC - Stack Overflow
https://stackoverflow.com/questions/21363187/git-show-dates-in-utc

 

先頭の 1つだけしか出力しないのだが、私の使い方だとそれで十分だ。

が、ちょっと欲が出るよね?
もうちょっと go.mod 風の出力にならんだろうかと。

$ TZ=UTC git log -n1  --abbrev-commit --abbrev=12 --date=format-local:%Y%m%d%H%M%S --format=format:%ad-%h
20211121014212-382e4677bbfb

コマンドラインが長いのだけど、どうせ記憶できる長さじゃないので alias にでも設定しておけばいいや。

2021/11/21

[golang] go.mod の pseudo-version の調べ方がわからん

わからんのだ。。。

 

golang で go.mod を作って require にて自分の GitHub リポジトリにあるコードを使う場合、 pseudo-version を書くのだろうと思っている。

https://golang.org/ref/mod#pseudo-versions

フォーマットはこんな感じ。

v0.0.0-<YYYYMMDDhhmmss>-<commit-id>

私が書くときはこういう順番でやっている。

  1. GitHub などで使いたい commit-id を調べる
  2. “YYYYMMDDhhmmss”のところは適当な日時にしておく
  3. “commit-id”のところに調べたcommit-idを書く
  4. go mod tidy
  5. commit-id の桁が長すぎると怒られるので、エラーメッセージに出力された文字列に置き換える
  6. go mod tidy
  7. タイムスタンプが違うといわれるので、エラーメッセージに出力された文字列に置き換える

commit-id は先頭から12文字分とってくればよいので、まだなんとかなると思うのだがタイムスタンプが分からんのだ。
コミットした日時をUTC変換すれば良いのだけど、GitHub だと秒数が出てこない。
clone してくればよいのだけど、めんどくさい。clone するくらいだったら↑でやってしまえばいいや、ということで今に至っているのであった。

go mod edit だとうまいことやってくれないかと期待したのだが、-require は少なくとも書いたとおりに go.mod に書くだけだった。
んー、大した作業ではないのだけど、なんかきれいにできないものか。

[golang][grpc]protocの準備

golang で gRPC をやりたい。

ということは、protobuf がいるので、protoc がいる。
準備しよう。


インストール手順があった。

Protocol Buffer Compiler Installation | gRPC
https://grpc.io/docs/protoc-installation/

これを書いている時点での最新版は v3.19.1。

https://github.com/protocolbuffers/protobuf/releases/tag/v3.19.1

私が動かしている Ubuntu にも protoc はインストールされていたのだが、あんまり新しくない。

$ protoc --version
libprotoc 3.6.1

GitHub の方からコンパイル済みの zipファイルをダウンロードして、$HOME/.local に展開する。

$ protoc --version
libprotoc 3.19.1

新しくなった。


golang で使うには protobuf compiler だけでは足りない。
というか、protoc はプラグインで各言語向けのソースを吐き出してくれるので、その準備がいる。

golang の場合は楽だ。

Quick start | Go | gRPC
https://grpc.io/docs/languages/go/quickstart/

今見ると、 protoc-gen-go@v1.26protoc-gen-go-grpc@v1.1 になっているが、protoc-gen-go の latestは v1.27.1 だった(protoc-gen-go-gprc の latestはv1.1.0だった。)。
v1.27.1 にした方がよいのかな? メジャーバージョンアップでもないし新しい方がよいかもしれん。困るまで v1.27.1 にしてみよう。
GOPATH は好きに設定すれば良いが、プロジェクトごとに GOPATH を設定しているならどこかにまとめておいた方がよいだろう。

 

Quick Start の順番にやっているので、次は example だ。
今は grpc-go の v1.41.0 を clone するよう書かれているが、最新は v1.42.0 だったのでそちらにした。

まずサーバ側を起動。

$ go run greeter_server/main.go
go: downloading github.com/golang/protobuf v1.4.3
go: downloading google.golang.org/protobuf v1.25.0
go: downloading golang.org/x/net v0.0.0-20200822124328-c89045814202
go: downloading google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98
go: downloading golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd
go: downloading golang.org/x/text v0.3.0
2021/11/21 13:21:11 server listening at [::]:50051

別のターミナルからクライアントを起動すると、たぶんサーバ側と通信している。

$ go run greeter_client/main.go
2021/11/21 13:22:03 Greeting: Hello world

こちらはサーバ側の出力。

2021/11/21 13:22:03 Received: world

うん、なんというか、protoc などを自分でやっていないので、gRPC したって気分になれないね。
というわけで、その次の gRPCサービスの更新をやろう。


さて、ファイルの構成はこんな感じ。

image

サーバとクライアントにそれぞれ main.go があって、あとは protoファイルとコンパイルされた pb.go と _grpc.pb.go がある。
ただの pb.go の方は protobuf のシリアライズ/デシリアライズの処理、_grpc.pb.go の方は gRPCのサーバ/クライアントの処理になっているそうだ(basics tutorial)。

あまり深く考えず、proto ファイルに SayHelloAgain()を追加しよう。
そして protoc でコンパイル。
--go_ が pb.go で --go-grpc_ が _grpc.pb.go になる。paths=source_relative は相対パスの場所にファイル生成する。

protoc でコンパイルできたら、それぞれの main.go を変更して同じように実行させると SayHelloAgain が動いていることが確認できるだろう。


ツールとしてはこのくらいあれば golang で gRPC できることがわかった。
あとは・・・知識がいるだけである。

[golang, github]go.modのmoduleを実物と違うのにしてよいのか

go.mod に慣れていないせいかもしれないが(golang自体にも慣れていないが)、非常に難しく感じる。

文句を言っても仕方ない(か、golangのプルリクを出すとか)ので、理解に努めよう。

$ go version
go version go1.17.3 linux/amd64


golang でオリジナルのものを作るときは気にならないかもしれないが、GitHub で fork したり mirror したりして一部を書き換えたいとなると思考が止まってしまう。

 

よくあるのが、git cloneする中でも改造して使いたいのはその一部だけ、というパターンだろう。
例として、fmt.Printf するだけのリポジトリを用意した。

https://github.com/hirokuma/gogo-test1/tree/v1

main.go と gogo/gogo1.go だけだ。

ここの gogoパッケージにある Gogo() だけ使うリポジトリがこちら。

https://github.com/hirokuma/gogo-test2/tree/v1

特に変な動作はしない。

go.mod は自分で記述したわけではなく、 go tidy で勝手に追加されたように思う。

それはよいのだが、gogo-test2 の方を go build . でビルドすると gogo-test2 というバイナリファイルができあがる。
これはなぜかというと、go.mod の module がそういう名前になっているからだ。
もしここで、オリジナルと同じ gogo-test1 という名前にしたくて go.mod の module を github.com/hirokuma/gogo-test1 にすると go build . は失敗する。

$ go build .
main.go:6:2: no required module provides package github.com/hirokuma/gogo-test1/gogo; to add it:
        go get github.com/hirokuma/gogo-test1/gogo

main.go:6 は import "github.com/hirokuma/gogo-test1/gogo" している箇所だ。

ではオリジナルと同じ名前のバイナリファイルを作るのには go build だけで無理なのかというとそうでもない。
あまり理解できていないのだが module path とか module directory という考え方があるようで、うーん、後ろに v2 とか書けばいけるのかな?

https://github.com/hirokuma/gogo-test2/blob/v2/go.mod#L1

いけた。

自分より前に v2 を使っている人がいたら私が v3 にしないといけないかというと、たぶんそういうことではないはずだ。調べるのは無理だろうし。
もし v2 でダメになるのはオリジナルが v2 などにした場合だろう。

 

さて、 gogo-test2 から gogo-test1/gogo にある Gogo() を呼び出していたが、自分で gogo.Gogo() を定義し直したいとしよう。

https://github.com/hirokuma/gogo-test2/blob/v3/gogo/gogo1.go

これを実行すると、gogo-test1 の Gogo() が呼び出される。

$ go run .
gogo-test2!
gogo!

なぜかというと、gogo-test2/main.go で import しているのが gogo-test1/gogo だからだ。
では、go.mod で置き換えてみよう。

https://github.com/hirokuma/gogo-test2/blob/v4/go.mod

うまくいくかと思ったが、これではダメだった。

$ go run .
gogo-test2!
gogo!

main.go の import を変更すればよいと思うのだが、本体の変更をせずに go.mod だけで管理できるようになっていると思うのだよ、私は。

難しいねぇ。続きは明日だ。


明日になった。続けよう。
ここまでを整理しておこう。

gogo-test2/v3 から追加した gogo/gogo1.go を main.go で参照させたいのだが、うまくいっていない。
gogo-test2/v4 で replace を追加したのだが、やっぱり参照してくれない。
もちろん、main.go の import を gogo-test2 に変更すれば参照する。

あれ、もしかしたらそれで問題ないのか?
そもそも gogo を自分で実装したものを使う時点で gogo-test1 とは何の関係もなくなっているのだ。
なので、関係をなくした v5を作った。

https://github.com/hirokuma/gogo-test2/compare/v4...v5

これを gogo-test1 の人が見て、あら私もこっちの gogo を使ってみたいわ、と思ったらこうなる。

https://github.com/hirokuma/gogo-test1/compare/v1...v2

うーん、これでよいような気もする。

 

 

では、gogo-test2 の module を yoshio に変更してみよう。

https://github.com/hirokuma/gogo-test2/compare/v5...v6

go.mod を変更すると main.go の import も変更しないといけなかった。
しかし、これだと git clone しないといけないのだったら gogo-test2.git だとわからないんじゃなかろうか?

gogo-test1/v2 の go.mod を gogo-test2/v6 に変更したらやはりエラーになった。

$ go get
go: github.com/hirokuma/gogo-test2@v0.0.0-20211121012830-b239fb1fd1ae: parsing go.mod:
        module declares its path as: github.com/hirokuma/yoshio
                but was required as: github.com/hirokuma/gogo-test2

こういう場合も replace で対応するようだ。

https://github.com/hirokuma/gogo-test1/compare/v2...v3

なので、module と実物のパスが異なってもなんとかなりそうだ。

2021/11/20

[git, golang]GitHubのorganizationのprivateリポジトリをgo getする

golang で作業している。
お仕事なのでプライベートリポジトリなのだが、場所が個人ではなくorganizationにある。

さて go getしたいのだが、organization には Personal Access Token (PAT)はない。
どうしたらよかろうか?

$ go version
go version go1.17.3 linux/amd64


検索して出てきたのは2つの方式だった(他にもあると思うけど)。

  • PATでアクセス
  • SSHにする

PAT方式で出てきたのはこちら。

https://zenn.dev/shootacean/articles/go-get-from-github-private-repository

organizationだからPATはないと書いたが、ここでは個人アカウントのPATを使う。
つまり、個人アカウントだろうとorganizationだろうと気にするなということだ。

これでできた。
GOPRIVATEは設定しなくても特にエラーは出なかった。

 

SSH方式で出てきたのはこちら。

https://gist.github.com/magnetikonline/3ceec9ece11375393ab5781d9370ee99

こちらはダメだった。
git clone で https://github.com で始まるリポジトリ名を打ち込むと SSHのパスフレーズを要求されたのでできるかと思ったのだけど、go getの方は「git@github.com: Permission denied (publickey)」としばしば見かけるエラーになった。

表示しているコマンドもcloneしているだけだし、そのまま打ち込んでもやっぱりパスフレーズ要求になったから、stdinを止められてるとかでダメなんだろうか。

~/.netrc で入力を回避するやり方も出ていたので、それと併用するのかもしれんが、試してはいない。

 

というわけで、「GitHubのorganization のプライベートリポジトリを go get するとき、個人アカウントのプライベートリポジトリをPATを使って go getするやり方と同じで取得できた」ということにしておく。

 

GOPRIVATEがいるのは、おそらく go.mod の中にプライベートリポジトリがある場合だと思う。
go get してエラーになるようだったら、GOPRIVATEに github.com/xxx みたいなのを設定すればできた。
全部同じ organizationのプライベートリポジトリだったのだけど、複数の organization にまたがってかつプライベートリポジトリとかだとわからんね。そんなことをするなってことだろうけど。

2021/10/31

docker再びの再び (5)

docker のネットワークについて勉強中。
前回はドライバの bridge で、今回は host だ。

Networking using the host network | Docker Documentation
https://docs.docker.com/network/network-tutorial-host/


ホストとコンテナの分離を削除する、という表現で混乱したのだが、ホストからコンテナのネットワークにアクセスできるということだと思っている。
アクセスだけなら ping でもできていたのだが、ここではもっとアプリ層に近いところだ。

チュートリアルでは nginx (えんじんえっくす、と読むのを知ったのはずいぶん最近のことだ)のコンテナを立ち上げて、ホスト側のブラウザで 80番ポート(デフォルトの http)にアクセスしている。

この手順では、ポート番号が出てきていないのだが、コンテナの外からホスト環境で nginx が立ち上がっているかのようにアクセスできるのだ。私の場合、Windows10 から VirtualBox で Lubuntu をゲストOS として起動し、ゲストOS 上で docker を立ち上げているのだが、Windows10 側から Lubuntu の IPアドレスをブラウザで指定してもアクセスできる。
実体はコンテナ側で動いているので、ポート番号だけ間借りしているというか転送しているというか、そんな感じなのか。


さて、 デフォルトの bridge を使うと他のコンテナやホストの間で通信ができ、ユーザ定義の bridge を使うと特定のコンテナの間で通信ができた。 1つのコンテナが デフォルトとユーザ定義の bridge の両方に接続?することができるので、そうするとどっちにもアクセスできた。

じゃあ、host とか使わずにデフォルトの bridge を使ってもいいんじゃ無いかと思ったのだが、これだと ss -nat | grep :80 などとしても 80番ポートが見当たらない。
docker ps で見ると PORTS に 80/tcp と出てくるので、80番ポートが使われているのは間違いないだろう。
しかし、 host を使った場合に docker ps で見ても PORTS には何も出てこない。

 

これだけだと bridge の意味がよくわからんのだが。。。
と思ったら、ブラウザでコンテナの IPアドレスにアクセスすると ss コマンドで現れた。 Listen していないのにアクセスできる・・・?
いや 80番ポートで Listenしているわけではないというだけかもしれない。
ss -l | grep docker とすると6つくらい出てきたので、今はそういうことにしておこう。


私の今のイメージでは、bridge と host はこんな感じだ。
丸いのがコンテナね。

image

  • bridge に名前を付けて作成すると、それと接続しているコンテナ間で LANを形成できる。
  • コンテナは複数の bridge に接続できる
  • host を指定するとホスト環境のネットワークとして見える

bridge と host も同時にできるかと思ったが、それはエラーになった。

bridge接続済みのコンテナを host につなごうとしたとき
container cannot be disconnected from host network or connected to host network

host接続済みのコンテナを bridge につなごうとしたとき
container sharing network namespace with another container or host cannot be connected to any other network

ホストのネットワークなら bridge のコンテナ内からでもアクセスできるので必要ないのだろう。

Connect a container to a user-defined bridge
https://docs.docker.com/network/bridge/#connect-a-container-to-a-user-defined-bridge

--publish を使うことで特定のポートをホスト側に見せることができる。だから host を使わなくてもよいというか、なるべく bridge を使って公開するポートを限定した方が不安が少ないと思う。


他にもいろいろあるだろうが、テスト環境を作るのであればこのくらいの知識でなんとかなるんじゃなかろうか。

2021/10/30

docker再びの再び (4)

4回もやっていると何でこんなタイトルだったのか忘れてしまいそうになるが、今回で dockerを勉強するのが3回目になるからだった。


今回はネットワークについて。

Docker コンテナ・ネットワークの理解 — Docker-docs-ja 19.03 ドキュメント
http://docs.docker.jp/engine/userguide/networking/dockernetworks.html

日本語だ。
多少情報が古いかもしれないが、基本的なことは変わってないんじゃなかろうか。まあ、後で調べよう。

 

ifconfig で見えるのは docker0 だけだが、docker コマンドでは書いてあるように 3つ見える。

$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
cc02564a80e1   bridge    bridge    local
4dee0c98d161   host      host      local
51c798d9077c   none      null      local

うーん、少し読んだけどさっぱりわからんよ。
もっと概要から説明してほしいのだが。

Networking overview | Docker Documentation
https://docs.docker.com/network/

Dockerのnetworkingサブシステムは、driver を使って追加/削除できる(プラグイン方式)らしい。
デフォルトで存在したりコアネットワークとして提供されているドライバはこれら。

  • bridge
  • host
  • overlay
  • macvlan
  • none

さっきの network ls で出てきたものでも DRIVER として表示されているな。

 

bridge

これがデフォルトのネットワークdriver。
固有名詞じゃなさそうだから「ドライバ」でよいのかな。
どういう意味でデフォルトかというと、普通はこれが使われる、というデフォルトのようだ。そりゃそうだろうと言われそうだが「デフォルトで付属している(けどあまり使われない)」みたいなこともあるので気にしたのだ。

名前からすると、docker コンテナ内のネットワークとリアル環境のネットワークを橋渡ししてくれるのだろうか。
なんというか、そうだったら bridge 以外はいらないような気がするのだが、他は何をするんだろう?

 

host

   うーん?

For standalone containers, remove network isolation between the container and the Docker host, and use the host’s networking directly.
スタンドアロンコンテナの場合、コンテナとDockerホスト間のネットワーク分離を削除し、ホストのネットワークを直接使用します。

通常はコンテナとホストが分離されているけど、それを削除するということは、分離しなくなるということか?

 

overlay

複数の docker daemonをそれぞれ接続し、 swarm service?がそれぞれ通信できるようにする もの らしい。
うん、よくわからん。

1台の PCでは、docker daemonは 1つ動いているということになるんだろうか。
bridge と host では「standalone containers」と書いてあったのだが、それぞれのコンテナで docker daemonが 1つ動いているという扱いにするのなら、これがコンテナ間を直接つないでくれるのかもしれん。
swarm というのは知らん。後回し。

 

macvlan

MacOS 専用のドライバかと思ったが、MACアドレスの方を表しているようだ。
MACアドレスをコンテナに割り当てることができるらしい。ということはマシン外とアクセスするときに使えるということだろうか。それに MACアドレスということなら物理層とか下の方の階層になるはずなので他のドライバと併用できるとかだろうか。

まあ、用途として特殊になりそうだから後回しでいいかな。

 

none

host が外部と接続しないようにするドライバだったが、 none はネットワーク接続をしないというドライバらしい。
ネットワークドライバなのにネットワークに接続しないとは、なんか面白いものだ。

カスタムネットワークドライバと一緒に使うのが普通らしい。
よくわからんが、後回しで良い気がする。ただ、初期設定で動作しているのでよく使うものなのかも。

 

 

説明の中で「standalone containers」という言葉がしばしば出てくる。
スタンドアローンなのにコンテナは複数形なのだ。だから、スタンドアローンのコンテナが複数あって、それを bridge とかで接続するとか、それぞれが host とかで自分の中だけで運用するとか、そういう話なんじゃなかろうか。

あと、「swarm service」という言葉も出てくる。

【swarm】
群れ、群衆。群がる。

swarm mode というページがある。

Current versions of Docker include swarm mode for natively managing a cluster of Docker Engines called a swarm.
Dockerの現在のバージョンには、スウォームと呼ばれるDockerエンジンのクラスターをネイティブに管理するためのスウォームモードが含まれています。

クーベネイティス、だったか、複数のコンテナを使ったシステムをうまいこと立ち上げるのがあったと思うが、それで使うのだろうか? わからんことは後回しだ。


Networking with standalone containers | Docker Documentation
https://docs.docker.com/network/network-tutorial-standalone/

bridge ドライバがデフォルトだとあるが、ここでは「デフォルト bridge ネットワーク」と「ユーザ定義 bridge ネットワーク」の2つが書かれている。ドライバとは書いていないので別の話なのか。
デフォルトの bridge ネットワークは製品版には推奨しないそうだ。

このチュートリアルではどちらも見せてくれるそうなので、やってみるのが良かろう。

 

まず、デフォルトの方。
特に何か特別なことをするわけでもなく、コンテナを2つ立ち上げると IPアドレスがそれぞれホストとは異なるプライベートアドレスが割り当てられた(ホストは 192.168.0.0 だが、コンテナは 172.17.0.0 だった)。
そのコンテナの中からグローバルなIPアドレスにアクセスできるし、相手のコンテナにもアクセスできる。

そういえばホストにもアクセスできるのかは確認していなかった。
docker stop でコンテナを終わらせた状態なのだが「再開」はできるのだろうか? チュートリアルと同じ docker run のコマンドを打ち込んでも名前が既にあるとエラーになった。
コンテナを削除すれば良いそうだが、単に再開させるだけなら docker start でよいようだ。

で、コンテナの中からホストに向けて ping しても成功したし、ホストからコンテナに向けて ping しても成功した。
私の記憶では、ネットワークのことをやるのにポート番号をどうのこうの設定していた気がするのだが... まあ ping だからポート番号は関係ないというだけで、後から出てくるのか。
わからん。

 

次はユーザ定義の方。
docker network create で bridge をベースにして(?)新たなドライバを作っているのか。
この方式だと、ping 先としてコンテナ名を使うことができる(bridge の場合は IPアドレスだけだった)。

デフォルトがなんで製品に向かないかというと、コンテナを立ち上げてしまうだけでネットワークにアクセスできてしまうからだと思う。他にも理由はあるとは思うが、システムごとに network create して作るのが無難だろう。


長くなったので、Hostのチュートリアルは次回にしよう。

2021/10/21

docker再びの再び (3)

前回、Dockerfile を使って簡単な、本当に簡単なイメージを作るところまでやった。
RUN で実行した内容までイメージに含まれるから、準備はそこでやっておけというところだろう。

つまり、Dockerfile をもらった場合は、それで docker build してイメージを作って、docker run してやればよいということか。
しかし、Dockerfile だけでイメージを作ることができれば良いのだけど、GitHub に上がってるやつなんかは他のファイルもいるだろうから、コードとか気にせず使いたいだけだったらイメージそのものがほしいところだ。

そこで Docker Hubの出番か。


docker run するときも、ローカルにイメージがなければ docker pull でダウンロードしてくると書いてあった。
なので、私も動かしたいものを Docker Hub から探してきて、docker pull して docker run すれば動かせると思う。

 

困ったことに、これを動かしたい、というものが思いつかない。。。
仕方ないので、これにした。

https://hub.docker.com/r/btcpayserver/lightning/tags

$ docker pull btcpayserver/lightning:v0.10.1-2
v0.10.1-2: Pulling from btcpayserver/lightning
a330b6cecb98: Pull complete
d18683af84c4: Pull complete
d089f3521651: Pull complete
00b9a3bf1da8: Pull complete
c6394cf1c296: Pull complete
241733e1daf6: Pull complete
de9614f35f2b: Pull complete
735eb65d54fb: Pull complete
8047f9f53e51: Pull complete
c9ed1993582a: Pull complete
Digest: sha256:929386072091408c8574fdda9c5e059cf2846fdef82b194f810ddb2281e09ac9
Status: Downloaded newer image for btcpayserver/lightning:v0.10.1-2
docker.io/btcpayserver/lightning:v0.10.1-2

$ docker run btcpayserver/lightning:v0.10.1-2
Invalid combinaion of LIGHTNINGD_NETWORK and LIGHTNINGD_CHAIN. LIGHTNINGD_CHAIN should be btc or ltc. LIGHTNINGD_NETWORK should be mainnet, testnet or regtest.
ltc regtest is not supported

イメージは取ってくることができたが、単に docker run するだけではダメだった。
環境変数かな?

Docker run reference | Docker Documentation
https://docs.docker.com/engine/reference/run/#env-environment-variables

docker run の後に -e "xxx=yyy" の形か、export された環境変数なら -e xxx で指定できるようだ。

$ docker run -e "LIGHTNINGD_NETWORK=regtest" -e "LIGHTNINGD_CHAIN=btc" btcpayserver/lightning:v0.10.1-2
network=regtest added in /root/.lightning/config
rpc-file=/root/.lightning/lightning-rpc added to /root/.lightning/config
Installing bundled plugins
C-Lightning starting, listening on port 9735
2021-10-20T10:31:52.693Z UNUSUAL lightningd: Creating configuration directory /root/.lightning/regtest
2021-10-20T10:31:52.693Z UNUSUAL lightningd: Creating configuration directory /root/.lightning/regtest
2021-10-20T10:31:52.920Z INFO    database: Creating database
2021-10-20T10:31:53.000Z UNUSUAL hsmd: HSM: created new hsm_secret file

ちゃんとした動作環境を用意していないからここで止まってしまうが、 -e で指定すれば動くことは分かった。
ついでに bitcoind も動かせることを確認しよう。

https://hub.docker.com/r/btcpayserver/bitcoin/tags

$ docker pull btcpayserver/bitcoin:22.0-1-amd64
22.0-1-amd64: Pulling from btcpayserver/bitcoin
07aded7c29c6: Pull complete
d17b51f7cfe8: Pull complete
6d6d52f12809: Pull complete
e47859bdfcf2: Pull complete
a57e01ab8b33: Pull complete
Digest: sha256:aacdcf9419dc88d80fd130fc7d51dda46b1be323967798c113ff77b6b6eea74c
Status: Downloaded newer image for btcpayserver/bitcoin:22.0-1-amd64
docker.io/btcpayserver/bitcoin:22.0-1-amd64

$ docker run btcpayserver/bitcoin:22.0-1-amd64
2021-10-20T10:40:41Z Ignoring unknown configuration value mainnet
2021-10-20T10:40:41Z Bitcoin Core version v22.0.0 (release build)
2021-10-20T10:40:41Z Assuming ancestors of block 00000000000000000008a89e854d57e5667df88f1cdef6fde2fbca1de5b639ad have valid signatures.
(略)

立ち上がる。
立ち上がるのだが、設定ファイルを書き換えて動かしたい。しかしイメージは既にできあがった状態だ。
たしか、ボリュームをマウントする方法があったはずだ。


docker runのオプションにある。

VOLUME (shared filesystems)
https://docs.docker.com/engine/reference/run/#volume-shared-filesystems

-v=ローカルのディレクトリ:コンテナ側のディレクトリ、という形だ。
指定すると、確かにローカルのディレクトリにコンテナ側で作成されたファイルが入ってくることが分かった。

が、どうもこの Dockerfile というかイメージは設定ファイルも一緒に作るようで、設定ファイルを先に置いていても上書きされてしまう。

こういうのはどうしようもないな。
幸いなことに、設定ファイルを作る部分は Dockerfile の RUN ではなく CMD 側で実行されているようなので、例えば regtest用にしたいならこんな感じで変数を与えてやると良い。

$ docker run -e "BITCOIN_NETWORK=regtest" -v=`pwd`/bitcoin:/home/bitcoin/.bitcoin btcpayserver/bitcoin:22.0-1-amd64

"entrypoint.sh" のような名前のファイルは、コンテナ起動時の設定をするためによく使われると Dockerの本にも書いてあったような気がする。今思えば、会社にあったオライリーの Docker本はなかなかよさそうだったのでもっと読んでおけば良かった。

ともかく、docker pull して楽ができるとしても、カスタマイズをしたいならオリジナルが何をしているのかを知っていないといかん。
とは思ったものの、今回は Docker Hubのリポジトリとほぼ同じ名前で GitHub のリポジトリがあったからたどり着けたのだが、普通はどうするのだろうか? IMAGE LAYERのところを見ても詳細はわからんし。


あとは、ポート番号だ。
単に docker run だけした場合、どうもコンテナで起動したアプリのポート番号はホスト側には出てこないようなのだ。

EXPOSE (incoming ports)
https://docs.docker.com/engine/reference/run/#expose-incoming-ports

こういう「incomming」って、誰に向けての説明か悩むよね。コンテナを起動するコマンドが主体なのか、コマンドを実行するのはホストだからホストが主体なのか。

オプションは -p ホスト:コンテナ らしい。「Publish a container's port or a range of ports to the host」なので、コンテナのポートをホストに公開するオプションになるようだ。

$ docker run -e "BITCOIN_NETWORK=regtest" -v=`pwd`/bitcoin:/home/bitcoin/.bitcoin -p 18444:18444 -p 18443:18443 btcpayserver/bitcoin:22.0-1-amd64

こうするとホスト側で ss -nat などでポート番号の使用状況を確認すると 18443, 18444 が出てくるようになる。

ちなみに、実行中のコンテナにログインしてコマンドを実行したいと思うなら docker exec を使うとよいだろう。

$ docker ps
$ docker exec -it d33e6ab4e04f /bin/bash

"d33...04f"は実行中のコンテナIDで、doker ps などで確認できる。

 

ネットワーク関係は次回だな。

2021/10/20

docker再びの再び (2)

さて、docker本体のことをやっていこう。

あれからちょっと本を読んで勉強したのだが、私は「まずDockerfileから!」と思っていたのだが、コマンドラインからでもできるけどいちいち面倒だからDockerfileにする、という感じなのだな。
まあ、そういう意味ではDockerfileから始めるのも間違いではないのか。

今回はこういうバージョンで進めます。

$ docker --version
Docker version 20.10.7, build 20.10.7-0ubuntu1~20.04.2


基本に忠実にいこう。
というよりも、本家のドキュメントが一番わかりやすい気がする。

Docker overview | Docker Documentation
https://docs.docker.com/get-started/overview/

Docker objectsのところに動かし方の例が載っていた。 "docker run" でいろいろやってくれるそうだ

$ docker rm `docker ps -qa`
$ docker rmi `docker images -qa`
$ docker run -i -t ubuntu /bin/bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
7b1a6ab2e44d: Pull complete
Digest: sha256:626ffe58f6e7566e00254b638eb7e0f3b11d4da9675088f4781a50ae288f3322
Status: Downloaded newer image for ubuntu:latest
root@972d70ba7c5b:/#

最初の2コマンドは、残っていたコンテナやイメージを全部消したかっただけだ。
3つめのコマンドがサイトに書いてあった例である。
Dockerfile がなくても使えるのだが、引数が多くなると面倒だから Dockerfileにしよう、というところだろう。

コンテナはメインプロセス?が終了すると実行が終わると言うことだった(本に書いてあった)。
ここでは /bin/bash がそれに当たる。シェルだからユーザが何もしないと生き続ける。 exit などで終了するとコンテナも終了する。

root@972d70ba7c5b:/# exit
exit
$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
$ docker ps -a
CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS                      PORTS     NAMES
972d70ba7c5b   ubuntu    "/bin/bash"   14 minutes ago   Exited (0) 27 seconds ago             kind_austin

docker ps は結果が長くなって折り返して見づらいのだが、STATUS が Exited になっていることがわかる。

 

docker runのオプション

-i ... Keep STDIN open even if not attached
-t ... Allocate a pseudo-TTY

シェルを動かしているから、ターミナルを動かさないといかんし、入力させるから STDIN を開いたままにしないといかんし、ということかな。


イメージを自分で作るのはいずれやるとして、コマンドラインの使い方を覚えるのが面倒そうだから Dockerfile の書き方を先に調べよう。

Best practices for writing Dockerfiles | Docker Documentation
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

Dockerfile はイメージを自動的にビルドするためのテキストファイルと書いてあるので、実行を簡単にするものではない?
でも「contains all commands」と書いてあるので、なんとかなるのか?

$ cat Dockerfile
FROM ubuntu:20.04
CMD /bin/bash

こんな Dockerfileを作った。
CMD と RUN の違いがよくわからんが、RUN が事前準備で CMD が実行するメインプロセスだと思う。

$ docker build -t test:v1 .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM ubuntu:20.04
20.04: Pulling from library/ubuntu
7b1a6ab2e44d: Pull complete
Digest: sha256:626ffe58f6e7566e00254b638eb7e0f3b11d4da9675088f4781a50ae288f3322
Status: Downloaded newer image for ubuntu:20.04
  ---> ba6acccedd29
Step 2/2 : CMD /bin/bash
  ---> Running in 4500cd01b011
Removing intermediate container 4500cd01b011
  ---> b3180f0f44d8
Successfully built b3180f0f44d8
Successfully tagged test:v1

これで終わりだ。
CMD で書いたものが実行されたりはしていない・・・? いや Step 2/2 でやろうとしているな。

サンプル通りにやってみよう。

$ cat Dockerfile
FROM ubuntu:20.04
COPY /hello.txt /
RUN cat /hello.txt

$ cat hello.txt
Hello, World!

hello.txt はあらかじめカレントディレクトリに作ってある。

$ docker build -t test:v1 .
Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM ubuntu:20.04
  ---> ba6acccedd29
Step 2/3 : COPY /hello.txt /
  ---> cbc39019824a
Step 3/3 : RUN cat /hello.txt
  ---> Running in 178ad7dff873
Hello, World!
Removing intermediate container 178ad7dff873
  ---> b66dc3af8f2a
Successfully built b66dc3af8f2a
Successfully tagged test:v1

おお、実行されているではないか!
シェルはちょっと特殊だと思うから、なんかオプションがあったような気もする。

docker run したのと違い、これで実行すると docker images で test というリポジトリが見えた。

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
test         v1        b66dc3af8f2a   14 minutes ago   72.8MB
< none>       <none>    9cdcb8314e60   16 minutes ago   72.8MB
< none>       <none>    b3180f0f44d8   20 minutes ago   72.8MB
ubuntu       20.04     ba6acccedd29   4 days ago       72.8MB

イメージを自動的に作るという、あれだろう。
サイズが全部同じなのは、これらは ubuntu という layer の上に全部載っかってるからということでよいのかな?
わからん。

 

まず、簡単に分かることからやろう。
さっきは RUN で cat したが、 CMD でもいいんじゃなかろうか。

$ docker build -t test:v1 .
Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM ubuntu:20.04
20.04: Pulling from library/ubuntu
7b1a6ab2e44d: Pull complete
Digest: sha256:626ffe58f6e7566e00254b638eb7e0f3b11d4da9675088f4781a50ae288f3322
Status: Downloaded newer image for ubuntu:20.04
  ---> ba6acccedd29
Step 2/3 : COPY /hello.txt /
  ---> 4522c706bb17
Step 3/3 : CMD cat /hello.txt
  ---> Running in e50e77c5baa6
Removing intermediate container e50e77c5baa6
  ---> e71d4d3c2fe5
Successfully built e71d4d3c2fe5
Successfully tagged test:v1

うーん、Running と出力されているので、動いていないことは無いのだと思う。
こちら側の標準出力に出力させる理由がなかったとかだろうか。

RUN
https://docs.docker.com/engine/reference/builder/#run
RUN命令は、現在のイメージの上にある新しいレイヤーでコマンドを実行し、結果をコミットします。

CMD
https://docs.docker.com/engine/reference/builder/#cmd
CMDの主な目的は、実行中のコンテナーにデフォルトを提供することです。

CMDのデフォルトは、上で書いたメインプロセスと同じ意味だろう。
RUNの「結果をコミットします」は、イメージを作るときに実行結果もイメージに含めますよ、ということだろう。

$ docker run test:v1
Hello, World!

ほら、動いた。

2021/10/03

docker再びの再び (1) - 無料プランの話

2016年、2019年とdockerのことを勉強しては忘れる日々。

hiro99ma blog: docker ? (1)
https://blog.hirokuma.work/2016/08/docker-1.html

hiro99ma blog: docker ? (2)
https://blog.hirokuma.work/2016/08/docker-2.html

hiro99ma blog: Docker再び (1)
https://blog.hirokuma.work/2019/05/docker-1.html

hiro99ma blog: Docker再び (2)
https://blog.hirokuma.work/2020/03/docker-2.html

今も忘れた状態を維持している。
コマンドであれば、メモを残しておいてまねすれば良いのだが、dockerみたいなのはそうもいかないと思う。

いや、dockerもそういうレベルで落とし込まないと忘れるに決まっている。忘れないという前提で勉強するのに意味が無いことは、今までの記事でも分かる。

今シリーズは、なるべくそういう記事になるようがんばろう。


記事の前提だが、Linuxのみとする。
うちだとWindows10がホストで Virtual Boxに Ubuntuをインストールしていることが多い。今週には Windows11 がリリースされるようだが、うちのPCたちはどれも推奨環境を満たせなかった。ThinkPad T460sは期待したのだが、まさかCPUが引っかかるとは。。。
そういうのはともかく、Windows上での dockerではなく、Ubuntu上での dockerとする。

あとは、ハードウェアを操作するようなのはあまり考えないことにする。
VirtualBox でハードウェアとなると USBデバイスを動かしたいこともあるのだが、そういうのは無しだ。


こちらがGet Startedページ。

Get Started with Docker | Docker
https://www.docker.com/get-started

Windowsで見ているので、Docker Desptop(for Windows)と Docker Hubが見えている。
Ubuntuで動かすときにはコマンドしか使ったことがないのだが、そうなると Linux Engineというのがそれになるのか?

image

Linux Engineのページに行くと、Fedora, CentOS, AWS, Azure, Ubuntu, Debianが一覧に出てきた。

Explore Docker's Container Image Repository | Docker Hub
https://hub.docker.com/search?offering=community&operating_system=linux&q=&type=edition

ちなみに Desktop は、Windows と Macが 1つずつだ。

Dockerの利用が企業規模によっては有料になったという記事を見たのだが、2021年8月31日のブログに書いてあるのは Docker Desktopについてだけのように見える。

Docker is Updating and Extending Our Product Subscriptions - Docker Blog
https://www.docker.com/blog/updating-product-subscriptions/

No changes to Docker Engine」とも書いてあるので、Linux Engineを使うだけなら気にしなくてよいかもしれない。

 

ただ、Docker Hubというものについても今年アナウンスが出ていた記憶がある。
実のところ、Docker Hubのこともよく知らないのだが、Dockerのコンテナ?イメージ?を公開して置いておいたり、自分の開発しているものを置いておいたりできるようなものじゃなかったか。

ふだん、よく「dockerからコンテナを取ってきて」みたいな感じで、どこからか動かすものを取得しているのだけれども、あれは Docker Hubに公開してくれている誰かのリポジトリを使わせてもらっているのだと思っている。

とりあえず、1つアカウントを作ってみた。もちろん無料版だ。

image

このアンダーラインを引いたのが、Docker Hubに自分のリポジトリを持つことができる上限なのだろう。
「public」については unlimited としか書いていないが、設定を見るとこうなっていたので「private」は1つまでしか持てないのだろう。

image

お仕事で使うとなると、なんでもかんでも public にするわけにはいかんので、有料プランを選択するという選択もあるのだろう。


Dockerが、いや Docker社がここまで一般的に使用できる環境を提供し続けているというのは、非常に驚くべきことだと思っている。

デファクトスタンダードというか、dockerを利用しても環境縛りに思われないくらいの立ち位置にいるというのはすごい。
環境を作るということに特化しているのに「コンパイルはgccでやります」みたいなノリで「環境はdockerでつくります」になっているので、そのことに恐怖すら覚えるところだ。だいたい数年するといくつも同じようなサービスが立ち上がるものだけど、Dockerがこの立ち位置を維持できているのは不思議な気がする。

2021/09/04

[js]JSONっぽいデータをJavaScriptで読みたい

JSONのデータはこんな感じだ。

{
  "name": "Yoshio",
  "age": 92
}

 

JSONっぽいデータと書いたが、今回はこういうデータのことである。

{
  name: "Yoshio",
  age: 92
}

「key: value」と呼ぶとしたら、keyの部分にダブルクォーテーションがないタイプのデータである。
JavaScriptのコード中に書くようなデータと言えばよいか。

 

何でか知らんが、サーバがこういうデータを返してくるようなのだ。
それをJavaScriptで読んで処理をしたい。


一応取り込めたのだが、この方法はあまりよろしくないとさんざん書かれている。

const VALUE = "{name: 'Yoshio', age: 92}";
const result = eval(`(${VALUE})`);
console.log(JSON.stringify(result));

$ node ev.js
{"name":"Yoshio","age":92}

 

よろしくない理由は eval() を使うと危険なためである。

置き換えの例としてFunctionを使うよう書かれていた。

const VALUE = "{name: 'Yoshio', age: 92}";
const result = Function(`return ${VALUE}`)();
console.log(JSON.stringify(result));

$ node ev.js
{"name":"Yoshio","age":92}

 

このシンプルなデータだとFunctionで置き換えられたのだが、複雑になるとそうもいかないようなのだが、どうしたらよいかわからん。。。

2021/07/04

[js]import * は普通

前回、namespace にして export default して import させていた。
namespace は無しにして import * でも同じことはできるのだが、わざわざ namespace にしてみたのだ。

その理由は「ワイルドカードよりも特定の namespace を使った方がよいのではないか」という気持ちがあったからだ。ほら、不特定なものよりも特定した方が好まれるじゃないか。

しかし。。。

TypeScript: Documentation - Namespaces and Modules
https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html

太字で

we recommended modules over namespaces in modern code.

最新のコードでは、名前空間よりもモジュールを推奨しました。
(Google翻訳)

と書いてある。
「最新のコードでは」だから将来変わるのかもしれんが、2021/07/04 19:23 ではそうなっていた。

 

なので、ワイルドカードで指定しているとかそういうのはあんまあり考えなくて良いんだろうね。変に気を回して時間を掛けてしまったよ。

[js]ファイルを分けたい

大きいプログラムを作ると、ファイルを分けたくなるだろう。そうじゃなくても、機能ごとにファイルを分けたいだろうし。
まあ、私が分けたいのだ。


index.ts

01: import * as greet from './greet';
02: 
03: const hello: greet.Greet = greet.hello();
04: if (hello != null) {
05:     console.log(`hello: ${JSON.stringify(hello)}`);
06: }

greet/index.ts

01: export type Greet = {
02:     greet: string;
03:     time?: string;
04: };
05: 
06: export function hello(): Greet {
07:     return {greet: 'hello', time: 'allday'};
08: }
09: 
10: export default {}
  • greet ディレクトリにファイルがあるので、 import の from はそこを指定する。
    • ファイル名が index.ts なのでディレクトリの指定だけで済んでいる。
  • greet/index.ts は export default {} としたので、ファイルの中にある export 指定したものは同じ名前で export される。

という理解で正しいだろうか?

 

import の from より前の部分があまりわかってない。
こうか?

  • 個別に import するなら、 import {個別にコンマ区切り} from '~~'
  • 全部 import するなら、 import 名前 from '~~'

 

greet/index.ts から個別に importすると、こうなる。

01: import {Greet,hello} from './greet';
02: 
03: const msg: Greet = hello();
04: if (msg!= null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

列挙するのが面倒ではあるが、使用しているものがはっきりするので場合によってはわかりやすい。


じゃあ全部 import するので import Hello from './greet' でよいかというと、これはダメそうだ。
Hello.Greet や Hello.hello という指定ができなかったのだ。
「Cannot find namespace 'Hello'」というエラーになる。

namespace を使う場合はこういう書き方になるようだ。
import するときに「import Hello from './greet'」としたが、別に namespace名と同じにする必要はなさそうだった。

あと、namespace も export を指定しないと個別に import できない(個別の話。export default指定しているなら関係ない)。namespace を export しても、namespace の中身が全部 export されたことになるわけではなく、その中でも export を付けたものだけが外部から見えるようだ。

index.ts

01: import Hello from './greet';
02: 
03: const msg: Hello.Greet = Hello.hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

greet/index.ts

01: namespace Hello {
02:     export type Greet = {
03:         greet: string;
04:         time?: string;
05:     };
06: 
07:     export function hello(): Greet {
08:         return {greet: 'hello', time: 'allday'};
09:     }
10: }
11: export default Hello;

 

この greet/index.ts を import * した場合は namespace 名を使わなくても済むようだ。

index.ts

01: import * as Pochi from './greet';
02: 
03: const msg: Pochi.default.Greet = Pochi.default.hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

 

 

namespace を付けるなら、 export default ではなく個別にした方がよいのかな?と思ったが、そうすると import側でも個別にしないといけなかった。
1ファイルで複数の namespace を入れ込むことは(私の実装だと)やらないので、これは default のままの方が使い勝手がよいか。

index.ts

01: import {Hello} from './greet';
02: 
03: const msg: Hello.Greet = Hello.hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

greet/index.ts

01: namespace Hello {
02:     export type Greet = {
03:         greet: string;
04:         time?: string;
05:     };
06: 
07:     export function hello(): Greet {
08:         return {greet: 'hello', time: 'allday'};
09:     }
10: }
11: export {Hello};

 


import の後ろは、 * as xx だったり name だったり {name} だったりといろいろあるが、いったいなんなんだ。

ECMAScript 2015 Language Specification – ECMA-262 6th Edition
https://262.ecma-international.org/6.0/#sec-imports

たくさんあるので from を使うパターンだけ見ていこう。
以下、イタリック&下線の部分はさらに展開されるという意味である。

import ImportClause FromClause;

FromClause の方は種類がないので、先にそちらを。

from StringLiteral

順番に展開すると ModuleSpecifier → StringLiteral なのだが、まあよかろう。
つまり文字列だ。

ImportClause は5種類に分かれる。

  • ImportedDefaultBinding
  • * as BindingIdentifier
  • { ImportsList }
  • ImportedDefaultBinding , * as BindingIdentifier
  • ImportedDefaultBinding , { ImportsList }

たぶん BindingIdentifier は「勝手に割り当てて良い名称」だと思う。bind だから紐付ける名称になるのかな。

上3つが import でよく見る形式で、残り2つはその組み合わせだ。
使えるものを展開してるのはこちら。

import - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import

まあ、こっちの方がわかりやすいな。日本語だし。


これで基本的なところはできたんじゃなかろうか。

もう1つやりたいのが、export するファイルとそうじゃないファイルを分ける作業だ。
実装を見られて困るとかではなく、C言語で言うところのヘッダファイルとソースファイルに分けたいというだけである。

できるのだろうか?
なお、tsconfig.js は isolatedModules:true を指定しているものとする。

 

re-export というのでやってみた。

 

greet/index.d.ts

01: export type Greet = {
02:     greet: string;
03:     time?: string;
04: };

greet/hello.ts

01: import {Greet} from './index.d';
02: 
03: export function hello(): Greet {
04:     return {greet: 'hello', time: 'allday'};
05: }

 

greet/index.ts

01: export {hello} from './hello';
02: export type {Greet} from './index.d';

 

index.ts

01: import {hello,Greet} from './greet';
02: 
03: const msg: Greet = hello();
04: if (msg != null) {
05:     console.log(`hello: ${JSON.stringify(msg)}`);
06: }

うーん、無理やりやりましたという感じが拭えない。
それに呼び出し元の index.ts の方に修正が入るのはダメだ。

 

これは書き方としてダメだった。namespace 内ではダメだそうだ。

01: export namespace Hello {
02:     export {hello} from './hello';
03:     export type {Greet} from './index.d';
04: };
05: export default Hello;

 

これならいけるようだ。

01: import {hello as myHello} from './hello';
02: import {Greet as myGreet} from './index.d';
03: 
04: export namespace Hello {
05:     export const hello = myHello;
06:     export type Greet = myGreet;
07: };
08: export default Hello;

うーん・・・。もっとよいやり方がありそうな気がする。

 

というわけで、あきらめて本を買うことにした。

O'Reilly Japan - プログラミングTypeScript
https://www.oreilly.co.jp/books/9784873119045/

本の中身は書かないのだが、私が import あたりをうまく理解できない理由が分かってきた気がする。

「JavaScript」とか「TypeScript」とかで検索しながらやっていたけど、import というかモジュールの扱いについて過去の経緯がいろいろあり、ネットで見つけた情報はどの時点の JavaScript について書いているかによって変わってくるからだと思う。そういうのを知らずにネットで調べていたので、あっちの情報とこっちの情報をまぜこぜにしてうまくいかなかったとか、そんな感じになったんじゃなかろうか。

本の内容で勉強することの利点は、よくも悪くも情報が混ざらないことだと思うので、あやふやな知識でふらふら調べるよりはましなんじゃなかろうか。せっかくお金出したんだから、ましであってほしい。

2021/07/03

[js]nullチェックとtruthy

JavaScript で null チェックする際、私はこう書くことが多い。

let val;

...

if (val) {
  // not null
  ...
} else {
  // null
  ...
}

C言語だと 0 以外の値なら真なのでアドレス値が入っているかどうかを判定するし、 Java だとそういう書き方は興られたような気がする。

で、JavaScript の場合はどういう仕様なのか?

 

Truthy - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
https://developer.mozilla.org/ja/docs/Glossary/Truthy

falsy として定義された値以外は truthy 扱いで、 truthy は boolean 判定するところに出てきたら true として扱うという仕様だった。

falsy はこれら。

  • false
  • 0
  • -0
  • 0n
  • ''(空文字列)
  • null
  • undefined
  • NaN

オブジェクトの場合は null か undefined かそれ以外になるはずなので、 if (オブジェクトの変数) だけで事足りるはずだ。


あとは、その書き方が一般的なのかどうか、だ。
私は if (変数) や if (!変数) の書き方で見慣れているのだが、一般的にそうでないなら使いどころは絞った方がよいと思うのだ。

nullとundefined - TypeScript Deep Dive 日本語版
https://typescript-jp.gitbook.io/deep-dive/recap/null-undefined

うーん「== null」って書いてあるな。

確かに、 != null だと null か undefined だけだが、変数名だけだと幅が広すぎるのか。
x && x.length > 0 とか x && x !== 0 だったら変数名だけでよいかもしれないが、やっぱりちゃんと書いた方が無難だろう。

 

というわけで、 null / undefined のチェックにはきちんと == null のように条件を書くことにしよう。


ついでに読んでいてわからなかったのがこちら。

ルートレベル(root level)のundefinedのチェック
https://typescript-jp.gitbook.io/deep-dive/recap/null-undefined#rtoreberuroot-levelnoundefinednochekku

null だろうと undefined だろうと、 x.y を参照するのに x が どこも指していないのだったらダメだろう。

undefined かどうかをチェックするには確かに typeof するしかないのだが、 null は別によいってことなんだろうか。

うーん、わからん。。。

 

ごにょごにょ考えたが、あれは x.y != null みたいなチェックをいきなりするのではなく、先に x をチェックしろということを言いたいだけなんだろうか。
それならわかるけど、やっぱり x != null だけでよいと思うのだ。

Checking for root level undefined
https://basarat.gitbook.io/typescript/recap/null-undefined#checking-for-root-level-undefined

原文も同じ。

まあいいや。
ルートレベルより下のプロパティやメソッドにアクセスする前にはルートレベルのチェックをするか try-catch を考慮するようにしておけば良かろう。

TypeScriptとnull/undefinedチェック

JavaScript のことを書いているサイトであればほぼ書かれていると思われる null/undefined チェック問題。問題というよりは FAQ なんだろうけど、私は何度やっても覚えられない。


まず、 null と undefined の型だが、これはオブジェクトではなくプリミティブ型とのこと。昔ちょっと JavaScript を書いたときは 'undefined' と文字列になっていたような気がするが、そう書いてある()からそうなんだろう。

にもかかわらず、 typeof null は "object" だ。 typeof undefined は "undefined" である。
nullは「プリミティブ値」と表現されていたし、undefined も同様だ

Primitive (プリミティブ) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
https://developer.mozilla.org/ja/docs/Glossary/Primitive

undefinedはプリミティブデータ型なので typeof で "undefined" になるが、 null は「プリミティブに見える」と微妙な表現をされている特殊なオブジェクトだそうな。あらゆるオブジェクトは nullから派生するらしい。

 

等値演算子(==, !=)を使う場合、null と undefined は同類と見なされる。

01: let val = null;
02: 
03: if (val) {
04:   console.log('defined');
05: } else {
06:   console.log('undefined');
07: }

これはどっちを通ったかというと、"undefined" の方を通った。 1行目が undefined でも同じだ。

null と undefined を区別したいなら同値演算子(===, !==)を使う。

A user name of null?
https://developer.mozilla.org/ja/docs/Learn/Getting_started_with_the_web/JavaScript_basics#a_user_name_of_null

この例は (!myName || myName === null) となっているのだが、 !myName だけでよいのでは?
もし && だったら myName === null だけでよいだろうし。

 

  • undefined はプリミティブデータ型でありプリミティブ値である
  • null はプリミティブ値のようでありオブジェクトのようであり、特殊な扱い
  • undefined と null を同じ扱いにして良いなら等値演算子(==, !=)で扱える
    • x == null とか x == undefined とか
    • !x でもよい
  • undefined かどうかを調べたいなら typeof === 'undefined' にする
    • null の typeof は 'object' になる

 

ちなみに、変数に undefined を与えて console.log したのが上、 'undefined' を与えて console.log したのが下。

image

null だとどっちで与えても白い文字で出力された。 うーん。

console.log なのか JavaScript 全体なのか分からないけど、人間寄りになるように動いてしまうのが苦手意識を持ってしまう原因かもしれん(私がね)。


TypeScript は型定義があるため、もうちょっと複雑になりそうだ。

nullとundefined - TypeScript Deep Dive 日本語版
https://typescript-jp.gitbook.io/deep-dive/recap/null-undefined

等値演算子で null チェックすればよいよ、ということだ。

といっても、ここは TypeScript の仕様説明ではないので、もうちょっと確認しよう。

 

まず、 null が型の方として扱われるようだ。

let val: number = null;

これはコンパイルエラーだ。どうしても代入したければ、私は null も OKですよ、ということにするしかないのか。

let val: number | null = null;

ちなみに、これもダメだ。

let val: number = null as number;

 

null は型扱いなので、変数を null型で指定できるようだ。

let val: null = null;
val = 'hello';

こんなのは代入する行でコンパイルエラーになる。 undefined もそうだった。

let val: undefined;
val = null;

使い道はないけどね。

 

だから、 null や undefined を代入したいのなら型のところで | null のような形にすることになる。
なるのだが、 undefined はデフォルト値になるので、ちょっと特殊な扱いかもしれない。その代わり、代入しないまま使おうとしたら、 | undefined になっていない限りはエラーになる。型と異なる変数が入っている、という扱いかもしれないが、TS2454 コンパイルエラーは before being assigned なので別エラーにはなっている。


C言語をよく使う人からすると、使い終わった変数に null を代入したいという気持ちに駆られることがある。

開いたファイルを扱うオブジェクトの変数があって、その変数が null だったら先にオープンしてから処理を続ける、みたいな。それをやろうとすると、最初は undefined だからよいとして、一度オープンして、クローズしたら null にしたくなる。しかし null を代入させるなら | null で許容させる必要がある。

私の期分の問題かもしれないが、 | null というか、型を複数割り当てるというのがどうも苦手だ。 string | number とかされたら「どっちが入ってんだよ!」ってなるからやらないけど、似たような気持ちになってしまう。

なら指定しなければ良いのだが、そうじゃなかったら boolean の変数を用意して判断するみたいなことになるだろう。でも、そこまでするのはなんか大げさな気がしてしまう。

なので、あまり選びたくない2択のどちらかをせねばならぬ。
あるいは、そういう設計の仕方自体が JavaScript らしくないということかもしれない。

 

そういう、言語仕様ではなく、センスというかお作法というかがよくわかっていないというのが最大の悩みであった。

[js]Arrayと配列と型付き配列 (2)

こんな話を2回に分ける必要もないのだが、日記だから良いのだ。

Arrayクラスと型付き配列は別物っぽいという話だ。

JavaScript 型付き配列 - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Typed_arrays

型付き配列というのは、いわゆる C言語などでの配列に近いもののようである。ただ、C言語では単に型のデータを配列にするのだが、型付き配列は「xxx型の配列のクラス」であり、その型ごとにクラスが用意されているところが異なる。

なので、定義されたクラス以外は(デフォルトでは)存在しない。

  • Int8Array
  • Uint8Array
  • Uint8ClampedArray
  • Int16Array
  • Uint16Array
  • Int32Array
  • Uint32Array
  • Float32Array
  • Float64Array
  • BigInt64Array
  • BitUint64Array

まあ、こんだけあれば十分だね。

どうやら、JavaScript では最初に Arrayクラスがあって、その後で型付き配列ができたようだ。データがメモリとして連続しているという前提がないと高速化できないというのはよくわかる。

つまり、Arrayクラスとは別物であるし、成り立ちからしても Arrayクラスの派生ではないだろう。 ArrayBufferクラスというものがあるようなので、むしろそちらが基底クラスになっているだろうか? しかし分類としては Array側になっていた。まあ配列だしね。

索引付きコレクション
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects#indexed_collections

なんかこう、Java のドキュメントのように継承関係の図はないんだろうか。

2021/07/02

[js]Arrayと配列と型付き配列 (1)

JavaScript というか TypeScript というか Node.js というか、とにかくその辺で実装をしている。

慣れない・・・。
同じことをするのに書き方がいくつかあるというのが混乱する。

特にアローね。

function x(val: number): number {
  return val + 2;
}

const x = (val) => val + 2;

アロー演算子でしか書けない場合があるのは良いとして、どっちでもよい場合が困るのだ。
function とか return とか、そこまで省略したいの??

ということを訴えてみたのだが、そういう言語だからしょうがないよな。
人間、あきらめも肝心だ。


今日は JavaScript で他のコードを読んでいて、知らない関数が出てきたので調べてみると、Arrayクラスの関数だったということがあった。
クラスだからメソッドだろうけど、そういうことではなく、import しないと使えないやつではない標準で提供されているものくらいは把握しておかないとつらいかろうということに気付いたのだ。

 

Array - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array

最初に書かれている解説で気になった言葉を箇条書きにする。

  • Arrayクラス
  • グローバルオブジェクト
  • 配列の構築に使用される
  • 型付き配列

まず、Arrayクラスはいいだろう。
「Arrayクラスはグローバルオブジェクト」と書いてあるので、importしなくても使えるのだろう。これもよい。
配列の構築に使用される、ということは、static関数みたいなのを持っていて、それで作ってあげたりするのだろう。

で、型付き配列とは挙動が異なるように読み取ることができる。

JavaScript 型付き配列 - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Typed_arrays

混同してはいけません、という文章があるので、ここに答があるに違いない!

が、今日は眠たいのでここまで。

2021/06/20

[js]PouchDB

前回の RxDB は、PouchDB のアダプタが使えますよ、といっていた。
今回はその Pouch DB だ。

PouchDB, the JavaScript Database that Syncs!
https://pouchdb.com/

簡単に使えるならなんでもいいんだけどね。


find() は別になっているのでインストールがいるのだった。

npm i pouchdb pouchdb-find

01: const PouchDB = require('pouchdb');
02: PouchDB.plugin(require('pouchdb-find'));
03: 
04: const db = new PouchDB('kittens');
05: 
06: const fn = async () => {
07:     db.bulkDocs([
08:         {
09:             name: "yoshida",
10:             color: "green",
11:             healthpoints: 30,
12:         },
13:         {
14:             name: "yoshio",
15:             color: "blown",
16:             healthpoints: 33,
17:         },
18:         {
19:             name: "momo",
20:             color: "pink",
21:             healthpoints: 9,
22:         },
23:     ]);
24: 
25:     await db.createIndex({
26:         index: {fields: ['healthpoints']}
27:     });
28:     const resHealth = await db.find({
29:         selector: {healthpoints: {$exists: true}},
30:         sort: ['healthpoints'],
31:     });
32:     for (let lp = 0; lp < resHealth.docs.length; lp++) {
33:         console.log(resHealth.docs[lp].name);
34:     }
35:     console.log();
36: 
37:     await db.createIndex({
38:         index: {fields: ['color']}
39:     });
40:     const resColor = await db.find({
41:         selector: {color: {$exists: true}},
42:         sort: ['color'],
43:     });
44:     for (let lp = 0; lp < resColor.docs.length; lp++) {
45:         console.log(resColor.docs[lp].name);
46:     }
47: 
48:     await db.destroy();
49: };
50: fn();

createIndex()が、fields を列挙したらその分作られるのかと思ったのだが、それぞれ作ることになるようだ。戻り値で"created" か "exists" が取ってこれるが、存在するなら作らないと書いてあるので、そんなに気にしなくてよいのか。

また、createIndex() した後に put() したら再度 createIndex() がいるのかと思ったが、そうでもないようだ。今のコレクションに対してインデックスを作るという意味ではないのか。

 

TypeScriptの場合はこんな書き方で良いのかな?
find() したドキュメントがうまいことプロパティにアクセスできなかったので as を使ってキャストしてしまった。よいのかどうかわからん。

01: import * as PouchDB from 'pouchdb';
02: 
03: PouchDB.plugin(require('pouchdb-find'));
04: 
05: const db = new PouchDB.default('kittens');
06: 
07: type dbType = {
08:     _id: string;
09:     _rev: string;
10:     name: string;
11:     color: string;
12:     healthpoints: number;
13: };
14: const fn = async () => {
15:     db.bulkDocs([
16:         {
17:             name: "yoshida",
18:             color: "green",
19:             healthpoints: 30,
20:         },
21:         {
22:             name: "yoshio",
23:             color: "blown",
24:             healthpoints: 33,
25:         },
26:         {
27:             name: "momo",
28:             color: "pink",
29:             healthpoints: 9,
30:         },
31:     ]);
32: 
33:     let idxRes = await db.createIndex({
34:         index: { fields: ['healthpoints'] }
35:     });
36:     const resHealth = await db.find({
37:         selector: { healthpoints: { $exists: true } },
38:         sort: ['healthpoints'],
39:     });
40:     for (let lp = 0; lp < resHealth.docs.length; lp++) {
41:         const r = resHealth.docs[lp] as dbType;
42:         console.log(r.name);
43:     }
44:     console.log();
45: 
46:     idxRes = await db.createIndex({
47:         index: { fields: ['color'] }
48:     });
49:     const resColor = await db.find({
50:         selector: { color: { $exists: true } },
51:         sort: ['color'],
52:     });
53:     for (let lp = 0; lp < resColor.docs.length; lp++) {
54:         const r = resColor.docs[lp] as dbType;
55:         console.log(r.name);
56:     }
57: 
58:     await db.destroy();
59: };
60: fn();

[js]RxDB

JavaScript でデータベースを使う必要性が出てきてしまった。
練習してみよう。


Introduction · RxDB - Documentation
https://rxdb.info/

NoSQLデータベースだそうな。SQLite か LMDB かでいえば、LMDB 側ということになろう。
私はそんなにデータベースを使ったことがないので、比較できるのはその2つくらいなのだ。

npm i でインストールすれば使えるのだろうと思ったが、RxDB 以外にもインストールしないと使えないらしい。
adapter というものもインストールして設定しないと使えないっぽい。
今回は Node.js なので、adapter のページにあった leveldown というものを使ってみよう。 Level DBらしい。

npm i rxdb rxjs leveldown pouchdb-adapter-leveldb

 

メニューの順番からすると、 RxDatabase, RxSchema, RxCollection, RxDocument, RxQuery の順にやっていくのだろう。

SQL ではないからか、テーブルではなくスキーマと呼ぶのか? そこはまだよいのだが、コレクションとかドキュメントとかあるのだが、何なのかが分からん。。。

開発者が知っておくべき、ドキュメント・データベースの基礎:特集:MongoDBで理解する「ドキュメント・データベース」の世界(前編)(1/3 ページ) - @IT
https://www.atmarkit.co.jp/ait/articles/1211/09/news056.html

ドキュメントはデータベースに突っ込むデータで、コレクションはその集合・・・?

  • createRxDatabase()でデータベースを作る
  • データベースに突っ込みたいデータの構造を決めて addCollections() で追加する
  • insert() でデータを突っ込む
  • find() でデータを取り出す
01: import {
02:   createRxDatabase,
03:   addRxPlugin,
04: } from 'rxdb';
05: 
06: addRxPlugin(require('pouchdb-adapter-leveldb'));
07: const leveldown = require('leveldown');
08: 
09: const fn = async () => {
10:   const db = await createRxDatabase({
11:     name: 'heroesdb',
12:     adapter: leveldown,
13:   });
14: 
15:   const myHeroSchema = {
16:     "title": "hero schema",
17:     "version": 0,
18:     "description": "describes a simple hero",
19:     "type": "object",
20:     "properties": {
21:       "name": {
22:         "type": "string",
23:         "primary": true
24:       },
25:       "color": {
26:         "type": "string"
27:       },
28:       "healthpoints": {
29:         "type": "number",
30:         "minimum": 0,
31:         "maximum": 100
32:       },
33:     },
34:     "required": ["color"],
35:     "attachments": {
36:       "encrypted": false
37:     },
38:     indexes: ['healthpoints', 'color']
39:   }
40:   await db.addCollections({
41:     heroes: {
42:       schema: myHeroSchema
43:     }
44:   });
45: 
46:   await db.heroes.bulkInsert([
47:     {
48:       name: "yoshida",
49:       color: "green",
50:       healthpoints: 30,
51:     },
52:     {
53:       name: "yoshio",
54:       color: "blown",
55:       healthpoints: 33,
56:     },
57:     {
58:       name: "momo",
59:       color: "pink",
60:       healthpoints: 9,
61:     },
62:   ]);
63: 
64:   const query = await db.heroes.find();
65:   const resHealth = await query.sort('healthpoints').exec();
66:   for (let lp = 0; lp < resHealth.length; lp++) {
67:     console.dir(resHealth[lp].get('name'));
68:   }
69:   console.log();
70:   const resColor = await query.sort('color').exec();
71:   for (let lp = 0; lp < resColor.length; lp++) {
72:     console.dir(resColor[lp].get('name'));
73:   }
74: 
75:   await query.remove();
76: }
77: fn();


難しすぎでは!?と思ったが、SQL を使うタイプだと SQL文というテキストになっているだけであんまり変わらんか。

addCollections() の戻り値はコレクションではあるのだが、コレクションの集合のようなものが返ってくるようだ。
これに find() したかったら、今回だと .heroes.find() のように名前を挟まないといかん。
そうなると db.heroes.find() と変わらん気がする。

 

それとは別に気になったのは、RxDB はその下で動いているデータベースを隠蔽するような形で動いていると思うのだが、トランザクションがあるデータベースだとパフォーマンスが悪くならないだろうか。 bulkInsert() で全部放り込めるなら良いけれども、取得しては保存、取得しては保存、みたいな使い方をしたい場合もあるだろうし。

2021/06/19

[js]非同期とコールバックとPromise

JavaScript で検索するとたくさん出てくる「コールバック Promise」。
この記事もその1つになるのだろう。自分の考えを整理するのに、どうしても文章に書かざるを得ない人たちの一人なので仕方がない。
ちなみに、ブラウザでの動作は考えておらず、Node.js から使うものとして書いている。


JavaScript はシングルスレッドだと言われている。仕様ではここになるのかな?

8.6 Agents
https://262.ecma-international.org/11.0/#sec-agents

An agent comprises a set of ECMAScript execution contexts,
an execution context stack,
a running execution context,
an Agent Record,
and an executing thread.
Except for the executing thread, the constituents of an agent belong exclusively to that agent.

【DeepL翻訳】
エージェントは、ECMAScript の実行コンテキストのセット、実行コンテキストスタック、実行中の実行コンテキスト、エージェントレコード、および実行スレッドから構成されます。実行スレッドを除き、エージェントの構成要素はそのエージェントにのみ属します。

実行しているコンテキストではスタックとかスレッドとかで構成されるけど、全部 a や an が付いているので1つしかないということなんじゃなかろうか。
「single thread」という単語は見つけられなかったのだよ。

解釈は間違ったかもしれんが、とりあえずシングルスレッドであることは信用しよう。


シングルスレッドなのに非同期ってどういうことじゃ、と思ったが、ここは割込みができるシステムと考えればよいと思う。

 

組み込み開発ではよくあることだが、CPU は実行状態として「通常モード」と「特権モード」のようなモードというか状態というかを持つことがある。電源を入れて起動直後は特権モードで、その状態じゃないとできない設定をいろいろやってから通常モードになって動き始める、みたいな感じだ。

通常モードで動いているけれども、例えば外部のセンサからINPUTピンに対して信号が来た場合には割り込んで良いようにする、という設定にしておくと、信号が来たら現在の状態をスタックに保存してから特権モードに切り替わり、特権モードで処理をして終わったらまた通常モードに戻る、というようなことができる。

こういう特権モードになるときは CPUとしてまるまる切り替わるので、スレッドとかそういうのはない。そして特権モードでの動作は通常モードとは異なるのでコンテキストが異なる。すなわち非同期だ。

 

私の場合は JavaScript の非同期をそんな世界観で眺めている。


非同期なところまではよいのだが、非同期で処理を何かしたいことがほとんどだろう。 setTimeout() みたいな遅延動作なんかはわかりやすい例で、何か動作を遅延させてから実行したいのだ。

しかし、実行し始めたコンテキストとは別のコンテキストで動作するので、素直に続きを行うわけにはいかない。コンソールにログを出す、みたいな影響を与えないものであれば気にしなくてよいのだが、変数に値を代入したりファイルに書き込んだりしたいことが多かろう。

グローバル変数みたいにスコープが広い変数であれば代入できると思うが、ローカル変数に戻したいという場合もあろう。そういうときは実行したい単位を関数にしてしまい、Promise<戻り値>を戻すようにして、asyncを付けておくと、呼び出し元はその関数を await つけて呼び出せば

 

例えば、このコードをそのまま実行すると、setTimeout()したログは一番最後に出力される。

01: console.log('hello');
02: setTimeout(() => { console.log('meme'); }, 1000);
03: console.log('world');

hello
world
meme

 

setTimeout()の後にworldを出したいだけなら、こんな感じか。
ちなみに TypeScript で書いていて、 tsconfig.json の "target" は "es2015" にしている。 tsc --init で作られたそのままだと怒られてダメだったのだよね。
vscode で実行していたのでデバッグ設定をしていたのだが、"Launch Program" で実行しても console.log の出力がどこにも出てこなくて悩んだ。実行するとタブが "TERMINAL" になるので、急いで "DEBUG CONSOLE"にすると出てきた。

01: async function setTimeoutPromise(msec: number): Promise<void> {
02:     return new Promise<void>((resolve: any, reject: any) => {
03:         setTimeout(() => { resolve();}, msec);
04:     });
05: }
06: 
07: const fn = async () => {
08:     console.log('hello');
09:     await setTimeoutPromise(1000);
10:     console.log('world');
11: };
12: fn();

なんかめんどくさいね。
await を使いたいがために async の関数が2つ必要になってしまった。

 

それはともかく、setTimeout() の第1引数は関数を受け取るタイプで、指定した時間後にこの関数を呼んでくれる。つまりコールバックだ。
これを呼び出し元で同期に見せかけたいので、Promise を使ってコールバックで resolve() を呼び出すようにしている。

 

自分で作った関数だけで非同期が発生することはなくて、そこからsetTimeout() なり npm でインストールしたライブラリなり、なんらかの外部に依存する場合しか非同期になることはないだろう。そして JavaScript で提供された関数を呼ばないで済むことはまずないので(たぶん変数の参照や代入だけか?)、前提条件はあまり考えなくて良いだろう。

 

非同期の基本的な処理完了通知はコールバックのはず。ただ、組み込み CPUでの割り込み処理で特権モードになったままになるのではなく、Node.js のコンテキストに戻されるはずだから、pthread の join で待って、戻ってきたらコールバック関数を呼び出す、みたいなことをやっているのか。

Promiseはその先の話で、いろいろ書き方はあるのだろうが、

  1. Promise を new するときにコールバックで結果を返す関数を最後に呼び出す関数を登録する
    1. 登録する関数の引数は (resolve, reject) にするのが一般的。
    2. コールバックが呼ばれるときに resolve() を呼び出す(失敗だったら reject() を呼び出す)。
    3. コールバックで結果を得てそれを次も使いたい場合は resolve() の引数にする。
  2. promise.then() を呼び出して、引数は resolve() の中身を実装するようなイメージの関数にする。
01: function setTimeoutPromise(message: string, msec: number): Promise<string> {
02:     return new Promise<string>((resolve: any, reject: any) => {
03:         setTimeout(() => { resolve(message);}, msec);
04:     });
05: }
06: 
07: console.log('hello');
08: setTimeoutPromise('world', 1000).then((msg: string) => {
09:     console.log(msg);
10: });

setTimeoutPromise()の引数をコールバック関数内の resolve()に渡せているのが面白いところだ。全然コンテキストが異なるならアクセスできないはずなので、うまいことやってるのだろう。深く探る必要もあるまい。

そして setTimeoutPromise() に async をつけて、呼び出すときに await をつけると、resolve() の引数がそのまま戻り値として使えるようになる。つまり同期になる。

 

なので、もうあまり難しいことは覚えずに async - await だけ覚えておけばいいかな、と思っていたのだ。


しかし、パフォーマンスを出さないといけないようになると、全部同期で済ませるのはもったいないこともある。非同期になる理由が「忙しいから」であれば待っていても良いのだが、「待ち時間が発生するため」の場合には、その間に他のことをやってしまいたいからだ。

 

まず、Promise.all()というものがある。

Promise.all() - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

Promiseのインスタンスを配列で受け取って、then でも配列で結果を返している。面白い。
これを async - await で書くとこんな感じか。

01: const promise1 = Promise.resolve(3);
02: const promise2 = 42;
03: const promise3 = new Promise((resolve, reject) => {
04:     setTimeout(resolve, 100, 'foo');
05: });
06: 
07: const fn = async () => {
08:     const result = await Promise.all([promise1, promise2, promise3]);
09:     console.log(result);
10: }
11: 
12: fn();
13: 

all()以外にもいろいろある

順番にやっていかないといけないものは async - await でつなげれば良いし、特に順番はどうでもよいなら Promise.all() でまとめてしまえばよい。本当にどうでもよいならコールバック関数が呼ばれておしまい、でよいのだが、そうもいかんことが多かろう。

 

これでなんとかなるかと思っているのだが、どうなんだろうね。苦労してみないと分からんのだろう。

2021/06/13

[android] HTTPでJSONを取りたい

Android用のライブラリAARが提供されていて、それを起動するとHTTPで内部的に通信をして取得したり指示したりするようになっているものがあるとする。まあ、今の私の状況なんだけどね。

permissionの設定を乗り越えて起動できるようになったのは良かったのだが、HTTPで内部でやりとりをする方法が分からん。

HTTP クライアントを選択する
https://developer.android.com/training/basics/network-ops/connecting#choose-http-client

ネットで検索すると HttpURLConnection がよく出てくるのだが、Retrofit というものを使う例になっているようだ。 HttpURLConnection の方が基本的な雰囲気をかもし出しているが、Retrofit だと JSON なんかもうまく扱ってくれそうなことが書いてあるので、黙って使うことにする。
ただ、概要の方には「HTTPライブラリVolley」と書いてあるので、実はそっちの方が良いのかも。。。
まあよい。必要ならやるだけだ。

Retrofit #Download
https://square.github.io/retrofit/#download

Gradleの場合は implementation を追加すれば良いそうだ。 Android Studio の Project Structure から 検索してもよさそうだ。"retrofit" で検索するとたくさん出てきたので、com.squareup.retrofit2 で始まっているものを選択した。

implementation 'com.squareup.retrofit2:retrofit:2.9.0'

Retrofit のサンプルでは GsonConverterFactory というものを使っていたので、そちらも書くとよいだろう。

implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

他にもコンバータがあるので、必要に応じて使い分けるのだろう。


Introduction に従えば何か動いてくれるんじゃなかろうか。

Introduction
https://square.github.io/retrofit/#introduction

baseUrl と GitHubService の定義からそれっぽい URL にアクセスしてみよう。

https://api.github.com/users/hirokuma

いろいろ返ってきた。

image

あちこちサイトを見たが、結局こちらのと同じような実装になった。

Retrofitの使い方・HTTPリクエスト・サーバー通信【Android・Kotlin】
https://101010.fun/programming/android-try-retrofit.html

私が作ったのはこちら。

https://github.com/hirokuma/AndroidHttpSample1

味も素っ気もないね。Android Studioでプロジェクトを新規作成したところからcommitすればよかったな。
最終的には同じようになってしまったが、一応 Android Developer での書き方と、 Retrofit のサンプルを参考にしていたのだ。サンプルは Android ではないので

 

Android Developerのように suspend fun にするとうまく動かせなかった。コルーチンにしなさい、みたいなエラーになるのだが、理解できていないのだ。

Android での Kotlin コルーチン  |  Android デベロッパー  |  Android Developers
https://developer.android.com/kotlin/coroutines?hl=ja

go言語にもあったが、軽量スレッドみたいなものらしい。昔はスレッドを軽量プロセスと呼んでたこともあったと思うが、スレッドですら生ぬるい時代になったのか。

 

suspend fun というのも何だかわかってないし。Java にはそういうのなかったような気がするが、最近できたんだろうか。
いろいろわかってなくてつらいな。

2021/06/06

[kotlin] はてな はnullable

少し前に、JavaScript の ?? や ?. の意味を調べた。

hiro99ma blog: [js]はてな、はてなはてな、はてなドット
https://blog.hirokuma.work/2021/05/js.html

C言語だと ! は否定、 ? は三項演算子の1番目、という意味しかなかったのだが、JavaScript はいろいろ意味があるんだなぁ、くらいに思っていた。

 

が、Kotlin にも ? がけっこう出てくることに気付いた。
最近の言語の流行りなんだろうか。

 

以前も載せた気がするのだが、もう私の記憶に残っていないからよしとしよう。

Keywords and operators | Kotlin
https://kotlinlang.org/docs/keyword-reference.html#operators-and-special-symbols

演算子なので operator だ。

? だけだと「これは nullableですよ」、 ?. は、nullではない場合だけ呼び出すという意味だそうな。

 


Kotlin にも === と !== があった。
referential equality という名前で、 a === b は a と b が同じオブジェクトを指している場合に trueになる。

 

JavaScript はどうだったかというと、 strict equality という名前で、日本語だと「厳密等価」と呼ぶそうだ。アルゴリズムに従って判定するとのこと。 x === y の場合、

  1. Type(x) と Type(y) が異なるならば false
  2. Type(x) が undefined なら true
  3. Type(x) が null なら true
  4. Type(x) が 数値の場合、
    1. x が NaN なら false
    2. y が NaN なら false
    3. x と y の数値が同じ値なら true
    4. x が +0、 y が -0 なら true
    5. x が -0、 y が +0 なら true
  5. Type(x) が 文字列の場合、 x と y が同じ文字列(同じ文字長かつ各位置の文字が一致)なら true、それ以外は false
  6. Type(x) が 真偽値(boolean)の場合、どちらも true か どちらも falseの場合は true、それ以外は false
  7. x と y が同じオブジェクトを指しているなら true、それ以外は false

 

7番目が kotlin の定義と同じなのだけど、じゃあ Kotlin でオブジェクト以外だと === が使えないということなのか? いやそもそも Kotlin は JavaScript を意識した言語なのだろうか?

そんなことを考えながらページを見ていったのだが 「JS」 というのがあることに気付いた。

image

意識しているとかなんとかではなく、

Kotlin for JavaScript | Kotlin
https://kotlinlang.org/docs/js-overview.html

Kotlin for JS という JavaScript からトランスパイルするものだそうな。
トランスパイルって何だろうと検索すると、TypeScript を JavaScript にトランスパイルする、とか、トランスパイラーとか、そんなのが出てきた。中間言語に置き換える、というのともちょっと違うのかもしれんが、昔の cfront みたいなものと思ってもよいのかもしれん。

ともかく、Kotlin for JS は transpile to JavaScript なので、Kotlin で書いたものを JavaScript に変換して、あとは JavaScript でやってもらうのだろう。立ち位置としては TypeScript と同じようなところにいるのか。まあ、Android などの場合も JVM に transpile するものだし、 Kotlin/Native は iOSも対象に入っているようなので、Kotlin が各ネイティブ言語へのラッパー言語のような戦略をとっているということか。 Java や .NET がやっていることよりも階層が上なので、各言語に対応する大変さはあるものの、その言語が使える環境であれば仮想的なものを用意せずに動かせるという利点がある。今のように「Android は Java」「iOS は Swift」のようにプラットフォームと言語がかなり固定化している時代に向いていると思う。 Kotlin で書いておけば「Android は Java やめます」ってなったとしても、 Kotlin から新しい環境へのトランスパイラさえ作ってあれば済むしね。大変だろうけど。

今回の === も、対応する概念がない言語の場合はトランスパイラがそういうコードを吐いてくれればよいだけなので、言語的な共通さを探すのはあまり意味が無いのかもしれんな。

[android] JavaからKotlinに変換したい

Androidアプリをやるからには、興味がなくても画面が多少いじれないとねー、と思って、手軽そう(な気がした)React Nativeを使ってみようとした。

Setting up the development environment · React Native
https://reactnative.dev/docs/environment-setup

Android Studioはある。Node もインストールしている。Windows 環境変数も設定している。
よし。

image

ちょっと見栄を張って(?)、TypeScript で作ってみる。

npx react-native init RNSample1 --template react-native-template-typescript

書いてあるとおりにやると、エミュレータが立ち上がってサンプルアプリが起動した。
ちなみに React Native は v0.64.2 のようだ。あまり npm のしくみをわかってないのだが、登録されている最新版を使ってくれているのかな?
(注:書いたときはv0.64.2だと思い込んでいたが、package.json を見ると v0.64.1 だった。何か見間違えていたらしい。 npm i react-native@0.64.2 まで指定するとちゃんと更新された。)


このテンプレートは Android の部分が Javaでできているので、どうせなら Kotlinにしておきたい。

テストアプリを作っていて気付いたのだが、Java のコードをクリップボードでペースとすると自動的に Kotlin変換して貼り付けるか聞いてくれるのだ。
すばらしい!

というわけで、プロジェクトを丸々 Kotlin に変換してくれないだろうか。

既存のプロジェクトに Kotlin を追加するにはどうすればよいですか?
https://developer.android.com/kotlin/faq#convert

Java ファイルを開いて、メニューから Convert を選んでやるとよいそうだ。
確認ダイアログが出たあと、やり方を聞いてくる。

image

これで OKを押すと変換され・・・あれ? 初回だからかもしれないけど build.gradle(ルートとappの2ファイル)しか更新されないぞ??

もう一度同じことをやると、今度は変換された。ファイルの拡張子も .kt にしてくれている。
まあよしとしよう(偉そうに...)。


しかし、ファイルを開いてからというのがちょっと面倒だ。
ディレクトリ以下まるまるやってくれないだろうか?

まず、1回目はツリーのトップにある app を選択して実行する。
同じダイアログが出てくるので、OK する。

image

 

2回目はここで実行。
よしよし、複数ファイル行ってくれた。

image

 

と、あっさりうまくいったように書いたが、この手順に落ち着くまでいろいろ失敗した。
ツリー表示の種類のせいか、選択しているディレクトリのせいかわからないけど、 build.gradle の plugins がどうのこうのというエラーが出ていたのだ。

まあ、うまくいったからよいのだ。

変換されたのは、 Java→Kotlin 以外だと build.gradle が 2ファイルだけだ。
何がどう変わったのかは、経験不足で分からん。 ext.kotlin_version とか kotrin-android とか、そんな感じ。

2021/06/05

[android] Googleドライブアプリにintentは投げられないのか

Android で ACTION_SEND で intent を投げるときに chooserでくるんでアプリ選択をユーザにさせるようにしたけれども、Googleドライブにしかファイルを保存させたくないという場合だったら直接Googleドライブアプリに intentを投げればいいんじゃなかろうか?

 

そう思って明示的インテントを作ろうとしたのだが、はて Googleドライブアプリのパッケージが分からない。

端末にインストールされているアプリ情報を取得する - すいはんぶろぐ.io
https://suihan74.github.io/posts/2021/01_09_00_get_installed_applications_info/

こちらを見て、getInstalledApplications() と getApplicationLabel() を組み合わせて一覧を作ってみたのだが、Android アプリ一覧で「ドライブ」に相当するラベル名がない。
パッケージ名も "drive" とか "drv" のようなものがない。まあこっちは略称が違うだけかもしれんが。

Android Studio が getInstalledApplications() で リンク付きの warningを出していたので見てみた。

Android 11 でのパッケージへのアクセス  |  Android デベロッパー  |  Android Developers
https://developer.android.com/about/versions/11/privacy/package-visibility

なんか難解な日本語だが、Android 11 からは Android 11 以降をターゲットとしているアプリは manifest に <queries> を書いておかないと一覧から外れてしまうことがあると言っている?

たぶん Googleドライブアプリのような AOSP にはない Googleアプリは Android 11 以降をターゲットにしていると思うが、メニューには出てくるしなぁ。でもメニューだから出てくると言うこともありそうだし。。。

 

うん、よくわからんが、そこまでして Googleドライブアプリに投げたいわけでもないので、よしとしよう。

[android] intent

前回は、テキストファイルを Googleドライブに対して共有することができた。
正確にいえば、ファイルを受け付けるパッケージに対して共有できた、になるのかな?

気にしているのは intentを投げたときに logcatに出てくる例外。出てくるだけで動作としては問題ないので困っている。 grantUriPermission()で READ_URI_PERMISSION を与えると出なくなるので、さらに困っている。動作しないなら対策しようとするし、ログが消せないならあきらめるのだが、どちらもできてしまうからだ。

何から調べるとよいかわからないので、まずは intentからいってみよう。


Android にいくつ基本要素があるか知らないが、 intent はその中でもかなり上位に位置するだろう。

インテントとインテント フィルタ  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/guide/components/intents-filters?hl=ja

前回は startActivity() を呼んだので、アクティビティの開始になる。特定のパッケージを指定しなくても、暗黙的インテントであればマッチするパッケージの候補を出してくれるのは助かるね。
それより前には startService() とか bindService() とかしたので、サービスの開始もやったことがあることになる。ブロードキャストはまだだな。

 

intent への設定項目は多数ある。

気になっていたのは setData() と putExtra()。どっちも intent先に渡すデータを設定しているように見えるのだが、違いが分からん。
読むと、データはまあデータで、エクストラは key-value のペアとのこと。前回の intentでは URIしか送るものはなかったのだが、それはデータではなくエクストラだった。

どこでその使い分けをするかというと、アクションくらいしかなさそうだ。。。と思ったのだが、下の例を見ていくと「明示的インテントの例」では送り先のパッケージを指定してデータを設定して startService()しているだけで、アクション指定はない。そういうやり方もあるのか。

ACTION_SEND を見ると、エクストラの記述がいろいろある。 ACTION_PICK は getData()と書いてあるので、送り元としては setData()してあげればよいということかな。公式というか共通というか、そういうアクションについてはそのページの Input のところを見てやればよいのだろう。

 

改めて ACTION_SEND を見よう。とにかく書いてあることが多い。

  • Activity Action
    • データを誰かに送信する
    • データの配信先は特定されていない
    • データの送信先をユーザに尋ねるのは、アクションの受信者次第
    • SEND intentを立ち上げるとき chooserでくるんであげると、ユーザがデータの送信方法を選択するプロンプトが出てくる(通常はそうする)
  • Input
    • MIMEタイプ
    • EXTRA_TEXT か EXTRA_STREAM + 送信データ
      • EXTRA_TEXTの場合、MIMEは "text/plain"
        • その場合、EXTRA_HTML_TEXT でクライアントがHTMLフォーマットでテキストを取得するようにもできる
      • それ以外なら EXTRA_STREAM
    • JELLY_BEAN(Android 4.1)以降であれば、データを setClipData()経由で送ることもできる
      • コンテンツを共有するときに FLAG_GRANT_READ_URI_PERMISSIONを使用できる
      • このアプローチを使用する場合、互換性のために EXTRA_TEXT か EXTRA_STREAM で同じデータを提供する必要がある。ClipDataを設定しない場合、 setActivity()を呼んだときにコピーされる。
    • Oreo(Android 8)から、CATEGORY_TYPED_OPENABLEが渡された場合、EXTRA_STREAM または setClipData()を介して渡された URIsは openTypedAssetFileDescriptor()を使用するあっセットタイプファイルとしてのみ開くことができる
    • 受信側が解釈できるオプションの標準エクストラは以下
      • EXTRA_EMAIL, EXTRA_CC, EXTRA_BCC, EXTRA_SUBJECT
  • Output
    • 無し
  • Constant Value
    • "android.intent.action.SEND"

機械的に訳した。

 

EXTRA_TEXT や EXTRA_STREAM で渡す方法だけでなく、ClipData経由でも渡すことができるのが grantの例外と関わっているのだろう。

まず ClipDataだが、これはクリップボードのデータ、というわけではないようだ。というのも間違いなのかな。クリップボードに保存したデータは ClipData扱いになるから、結果としては同じで良いのかな?

intentに ClipDataを紐付ける関数として setClipData()があるが、ClipDataに含まれる URIに対して FLAG_GRANT_READ_URI_PERMISSION や FLAG_GRANT_WRITE_URI_PERMISSION が機能するそうだ。

 

つまり、今回のように ClipData を使うつもりがない場合には気にしなくてよいはずだ!という結論に至った。私の解釈が間違ってなければね。

01:     fun sendIntent(view: View?) {
02:         val filename = "myfile.txt"
03:         val pathUri: Uri = FileProvider.getUriForFile(
04:             this,
05:             BuildConfig.APPLICATION_ID + ".fileprovider",
06:             File(filesDir, filename)
07:         )
08:         val shareIntent: Intent = Intent().apply {
09:             action = Intent.ACTION_SEND
10:             putExtra(Intent.EXTRA_STREAM, pathUri)
11:             type = "plain/text"
12:         }
13:         val chooseIntent: Intent = Intent.createChooser(shareIntent, null)
14:         // ClipDataを使う場合
15:         // grantUriPermission("android", pathUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
16:         startActivity(chooseIntent)
17:     }

AndroidStudio というか IntelliJ というかはショートカットキーでコメントアウトすると行頭になるのがちょっときになるね。思わず手動でやってしまった。