Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

Mac かつ VSCode で Idris の環境構築をする

Idris Advent Calendar 2020 3日目の記事です。

2020年今年の言語に Idris を選んで年始に Hello, World していたのですが、見事にその後 Python をしていた1年でした(仕事で使うことになったので Python を覚える必要が出てきました)。

完全に Idris は初心者ですが、Idris 楽しんでいきたいと思います!

今回は Mac ユーザー向けに Idris の開発環境構築を紹介します。

Idris とは

だいぶ雑に説明するなら、依存型つき Haskell です。詳しくは2020年 Idris Advent Calendar 1日目の記事をご覧ください。

環境

VSCode 上でコードが書けると安心できますね、ということで VSCode を使用する前提でいます。Idris は REPL が使えるので、それを利用するだけでも Hello, World くらいなら結構簡単にできますが。

Idris のインストールをする

Idris 自体を入れる

2つルートがあります。今回私は Homebrew を使用しました。一方で、公式には cabal によるインストール方法が記載されています。たぶんどちらでもいいと思います。

まず Homebrew から。

$ brew install idris

これが終わった後に、

$ idris --version

しましょう。私の環境では、

❯ idris --version
1.3.3

と返ってきました。Idris は1系と2系があり、2系は開発中なんだそうです。

cabal を入れる

cabal 自体はこのあと必要になるので、別途インストールしておきましょう。

$ brew install cabal-install

これを行ったあとにパスを通す必要があります。.zshrc などに下記を書き足しましょう。

PATH = "$HOME/.cabal/bin:$PATH"

stack の場合は試したことはないです。

VSCode の Extension を入れる

プラグインのインストール

VSCodeプラグインを入れましょう。Extensions で「Idris」と検索すると出てきます。

github.com

この README にしたがって、idringen というプロジェクトマネジメントツールを入れる必要がありそうです*1。プロジェクトマネジメントツールを入れるのは少々抵抗があるかもしれませんが、初心者の場合そもそも言語的なベストプラクティスも知らないと思うので、入れて勉強しようということで私は入れてみました。

最後のメンテが4年前だけど…

idringen を入れる

github.com

cabal を経由して入れます。

$ cabal install idringen

インストール後、下記を叩きます。

$ idrin --help

help コマンドは実装されていないらしい…。メンテのしがいがありそう。

❯ idrin --help
idrin: not support subcmd --help yet
CallStack (from HasCallStack):
  error, called at src/Idringen.hs:28:24 in drngn-0.1.0.3-b556eddc:Idringen

idringen でプロジェクトを作ってみる

sandbox プロジェクトを作りましょう。ちなみに、名前に「-」を入れると生成後エラーになってビルドできません。ご注意を。

new コマンドでプロジェクトを作成できます。

❯ idrin new sandbox
Creating new project named sandbox

Hello, World

ビルドして…

❯ idrin build

実行しましょう。

❯ idrin run
hello sandbox!

これで完了です!

VSCode で開いてみる

シンタックスハイライトもしっかりついていて完璧ですね!これで開発をスタートしていきましょう!

f:id:yuk1tyd:20201202114631p:plain
VSCode で Idris プロジェクトを開いてみた

(これだけ見ると、めっちゃ Haskell ですね)

Idris の本

『Type-Driven Development with Idris』という本があって、いろいろ詳しく書かれているように思います。

PDF も存在していて、下記です。

www.pdfdrive.com

12月中に時間があったら、この本からいくつかエクササイズして記事にしてみようかなと思っています。

*1:なんとExtensionの作者が作っています

2020年。あなたとJAVA、今すぐダウンロー

ド。(いつものやつ)

2020年時点で改めてよさそうな Java のインストール方法についてまとめておきます。

ちなみにですが、

  • Scala(sbt)を利用する。
  • Scala のプロダクトでは Java 8 を使用し、プライベートでは Java 14 を使用する。
  • Open JDK そのものの縛りは特にない。
  • macOS を使用する。

という前提のもと行う環境構築です。jenv を利用してバージョンの切り替えを行いながら開発することを前提としています。IntelliJ を使用しているとほとんど意識することはないんですが。

インストール方法

brew tap をして、AdoptOpenJDK を入れます。

$ brew tap AdoptOpenJDK/openjdk

JDK 8 なら、下記のように入力すると入れられます。

$ brew cask install adoptopenjdk8

AdoptOpenJDK という選択肢

という観点から、今回は AdoptOpenJDK を使用しています。

jenv で複数バージョンを管理する

Java は最近、バージョンリリースのサイクルが速くなりました。サクッと最新版を試したいけれど、普段のプロダクトでは少し古いバージョンを使用しているということもあるかと思います。

そういったときには jenv というツールが使えます。

github.com

brew install jenv

をして、

jenv versions

で、動いているかを確認します。その後、AdoptOpenJDK で入れたバージョンたちを次のようなコマンドで追加していくと、jenv の管轄下に置くことができます。

mkdir ~/.jenv/versions

ディレクトリがないと、↓でコマンドを叩いた際にディレクトリがないよと怒られます。

echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.bashrc

などを忘れずに。パスを通さないと、java -version したときに jenv の設定が読まれないみたいです。

$ brew cask install adoptopenjdk11
$ brew cask install adoptopenjdk10
$ brew cask install adoptopenjdk8

インストールした AdoptOpenJDK の分しか入れられないので注意。

これを、jenv に追加します。

$ jenv add /Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/
$ jenv add /Library/Java/JavaVirtualMachines/adoptopenjdk-10.jdk/Contents/Home/
$ jenv add /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/

Java 8 なら Java 8 を標準に設定して、

$ jenv global 8.0

これで、

$ java -version

をした際に、8に切り替わっていれば成功。ちなみに特定のディレクトリ配下だけ Java のバージョンを変える設定もできるみたいです。

参考記事: https://www.dhiller.de/2019/01/06/setup-multiple-jdks-with-jenv-and-adoptopenjdk.html

terraformを触り始める

画面をポチポチしながらインスタンスを立てて…といったことを考えるのが辛くなってきたので、そろそろ terraform 使いこなせるようになりたいと思い、チュートリアルをコツコツはじめてみました。

普段は CDK を使ってるんですが、最近少しだけ terraform を触るチャンスがあって、なかなかいいじゃないかと思ったのもあります。

あと基本的にインフラ(というかクラウド)が苦手なのですが、そろそろ逃げられなくなってきたのもあって、本腰入れてやらないとなと思っているというのもあります。

以下はこのチュートリアルをやってみたメモです。terraform にはチュートリアルガイドが存在し、それを一通りやるだけである程度 terraform を使いこなせるように設計されているようです。英語ですが、無料なのでオススメです。

terraform そのもののインストールコマンド等は一旦割愛します。詳しくはチュートリアルをご覧ください。私の手元の terraform のバージョンは下記です。

❯ terraform --version
Terraform v0.13.5

また1つ1つ調べていたら例のごとく長くなってしまったので、お好きなところをかいつまんでお読みください :) 私は terraform はまったくの素人なので、情報が正確でない箇所があるかもしれません。

今回のお題: Docker 上に nginx を立ち上げる

今回のお題は Docker 上に nginx を立てて localhost:8000 で Welcome to nginx! するものです。

terraform は設定内容を .tf という拡張子のファイルに書いていきます。さまざまなリソースの管理をコード形式で見られるように作られているようです。

さっそくですが、Docker 上に nginx を立ち上げる設定は下記のように書くことができます。main.tf という名前をチュートリアルに従って命名しました。

terraform {
  required_providers {
    docker = {
      source = "terraform-providers/docker"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name         = "nginx:latest"
  keep_locally = false
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.latest
  name  = "tutorial"
  ports {
    internal = 80
    external = 8000
  }
}

{} で囲まれたところをブロックと呼ぶそうです。terraform の tf ファイルの文法は比較的単純で、

<BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
  # Block body
  <IDENTIFIER> = <EXPRESSION> # Argument
}

というものを繰り返していくだけのようです。非常に覚えやすく、シンプルで好きです。

設定ファイルを理解していく

まず上から読んでいきます。

terraformブロック, required_providersブロック

terraform ブロックは、その terraform の実行単位でのさまざまな設定を行えるブロックです。今回は後ほど解説する required_providers のみを記載していますが、required_version のように terraform の実行バージョンを設定したりもできます*1

version を使用する場合には、たとえば

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 2.70"
    }
  }
}

といったように書けるようです。

今回記載されている required_providers ブロックは、どの「プロバイダ(provider)」を利用するかを規定するブロックになっています。プロバイダという概念については後述します。

required_provider 内には2つの識別子が存在します。

  • source address と呼ばれる required_provider にのみ定義できる識別子。
  • local name と呼ばれる terraform のモジュール内ならどこでも定義できる識別子。
  required_providers {
    docker = {
      source = "terraform-providers/docker"
    }

上記のような記述をした場合、

  • source = "terraform-providers/docker"部分にあるのが、先ほどで言うところの source address。この場合の source address は terraform-providers/docker となる。
  • docker = { 部分にあるのが、先ほどで言うところの local name。この場合の local name は docker となる。

なお名前がコンフリクトすることもあるようです。そして、terraform はどの名前空間に紐づくものかまでを詳細には推測できないようなので、できるだけがんばって名前がダブらないようにする必要があるようです。

このあたりの詳しい仕様はこちらのページに記載されています。上述ならびに以降も同様ですが、ほぼこのドキュメントの内容を日本語にまとめ直していきます。

provider ブロック

まずプロバイダ(provider)という概念を理解します。プロバイダというのは terraform 自体とは独立したプラグインです。なので、バージョン管理も terraform からは分離しており、独自のバージョニングを保持しています。

Terraform Registry というところに数多くのプロバイダが登録されており、terraform においてはそれらを利用しながら設定ファイルを組み上げていくことになります。たとえば AWS のプロバイダや、Docker のプロバイダといったメジャーどころはだいたい抑えられているように見えます。

provider で重要なことは下記です。

  • provider という宣言によって始まる。
  • local name で識別する。
  • provider ブロック自体は tf ファイルのルートに書く必要がある。

provider ブロックの基本的な文法は下記のような形でしょう。

provider "<LOCAL NAME>" { ... }

先ほど説明した local name が provider の横に入ります。つまり、required_providers で先ほど docker という local name を設定したので、provider にも docker が入る必要があります。したがって、下記のような記述になっています。

provider "docker" { ... }

これだけではわかりにくいので少し他の例も利用しておくと、provider ブロックの中には例えばリージョンの情報といった arguments が入れられます。どうやらプロバイダによって arguments は異なっているようです。たとえば AWS のプロバイダを使用してみたい場合は、次のように記述できます

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 2.70"
    }
  }
}

provider "aws" {
  profile = "default"
  region  = "us-west-2"
}

provider ブロックの中に独自の arguments が登場していることがわかります。

provider に関する詳細な仕様は、このページにあります。

resource ブロック

最後に登場してきたのは resource というブロックです。

リソース(resource)というのは、terraform が生成・管理する最小限の要素のことをいいます。たとえば先ほどの例ですと、docker image と docker container というリソースを terraform が生成し、管理することになります。

resource で重要なことは下記です。

  • リソースというのはプロバイダが提供する、インフラプラットフォームを管理するためのもの。
  • resource type というものが存在する。
  • local name が resource type ごとにさらに付与されている。

resource は次のような文法をもっています(と、推察しました)。

resource "<RESOURCE TYPE>" "<LOCAL NAME>" { ... }

今回は2つの設定を記述しました。

resource "docker_image" "nginx" {
  name         = "nginx:latest"
  keep_locally = false
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.latest
  name  = "tutorial"
  ports {
    internal = 80
    external = 8000
  }
}

上から順に紐解いていくと、

  • resource type が docker_image で、local name が nginx のリソースを作りたい。
  • resource type が docker_container で、local name が nginx のリソースを作りたい。

ということがわかります。

provider と resource の関係性をコードで示すと次のようになるかもしれません。

さて、この resource type 以下の情報は一体どこに書いてあるのか、というのが気になり始めると思います。これは Terraform Registry の各プロバイダのページに(それなりに)詳しく書いてあります。なので、開発していく際にはこれらを見ながらやっていく感じになるのでしょう(まだ初心者なのでわからないですが)。

実行

上記まで理解できたところで、main.tf がおそらく完成しているはずです。あとは、

terraform init

して、

terraform apply

して設定内容を反映し、localhost:8000 にアクセスすると、無事に nginx が立ち上がっているはずです!

terraform destroy 

で、apply にて生成したものを削除することができます。

terraform init

何をしているか気になったので、さっそく叩いてみました。

terraform init --help
Usage: terraform init [options] [DIR]

  Initialize a new or existing Terraform working directory by creating
  initial files, loading any remote state, downloading modules, etc.

  This is the first command that should be run for any new or existing
  Terraform configuration per machine. This sets up all the local data
  necessary to run Terraform that is typically not committed to version
  control.

  This command is always safe to run multiple times. Though subsequent runs
  may give errors, this command will never delete your configuration or
  state. Even so, if you have important information, please back it up prior
  to running this command, just in case.

  If no arguments are given, the configuration in this working directory
  is initialized.
  • 初期ファイルを設定したり、リモートにある状態をロードしたり、モジュールをダウンロードしたりするようです。
  • terraform を実行するのに必要なローカルデータを読み込むコマンドのようです。
  • 冪等のようです。

terraform apply

同様に何をしているか気になったので叩いてみました。

terraform apply --help
Usage: terraform apply [options] [DIR-OR-PLAN]

  Builds or changes infrastructure according to Terraform configuration
  files in DIR.

  By default, apply scans the current directory for the configuration
  and applies the changes appropriately. However, a path to another
  configuration or an execution plan can be provided. Execution plans can be
  used to only execute a pre-determined set of actions.
  • 指定ディレクトリに存在する terraform の設定ファイルを読み込んでビルドするか変更を更新するかを行ってくれるようです。
  • 脳死でも適切に変更を検知して状況を適用してくれますが、別の設定ファイルや実行計画を与えたりもできるようです(これは使ったことがないので、正しい説明を私がしているかわかりません)。

感想

  • たとえば AWS CDK は非常にいいツールではあるのだが、汎用プログラミング言語で書くとなると terraform のようにシンプルな宣言的な記法にはなかなかしづらい。
  • どうしてもクラスや関数と言った、本来的に設定ファイルの記述とは関係ない部分の影響を受ける。
  • もちろん、汎用プログラミング言語で書けるというのは、たとえばアプリケーションエンジニアにとっては、普段使っている言語でそのままインフラの設定まで書けるというメリットもある。これは大きい。
  • 一方でその点 terraform は宣言的な記法でインフラ構成を記述していける。
  • 宣言的な記法は、抽象化が難しいなど一長一短ありそうだが、terraform はそのあたりは上手に解決しているはず(と信じたい)。これからの勉強次第。

*1:terraform は version 0.12 と 0.13 でも結構違うなど、まあまあ破壊的変更が入っていて大変だと聞いたことがあります。

Rust の `!` (ビックリマーク、エクスクラメーションマーク、感嘆符、never) 型

タイトルは記号のググラビリティ確保のためにわざとつけています(笑)。動作はすべて、Rust 1.47.0 (stable) です。nightly を使用した場合は nightly と文中に記載しました。

TL; DR

  • Rust には ! という「何も返さないことを示すモノ」が存在する。
  • never 型と呼ぶ。
  • 値を何も返さない計算の表現として使用される。
  • 普段は型強制される(ので、使っていることに気づかないかも)。
  • never 型は安定化されていない。

今回も記事が長くなってしまいました。ちゃんと調べると、結構いろいろ書くべきことがありボリューミーになってしまいました。好きな場所をかいつまんでお読みください。

基本

! = never 型

! は Rust では never 型と呼ばれます。

これは他のプログラミング言語型理論などではボトム型あるいは発散型と呼ぶことがあります*1。他のプログラミング言語でも同様の概念が存在しています。Scala ではボトム型は Nothing として表現されます。TypeScript では Rust と同様に never として型をつけます

たとえば、

fn diverges() -> ! {
    panic!("This function never returns!");
}

この関数は値を何も返しません。それでは unit 型と同じなのでは?と思うかもしれませんが、unit 型は Rust においては中身のない Tuple という値なので、実質的に値を返していることと同義になります。そうではなく、never 型は文字通り値を何も返さないことを示します。

このように何も返さない関数を diverging functions と呼びます。この型は、関数や計算が発散していることを示しています。関数や計算が発散しているとは、つまり値を呼び出し側に返さないことを意味します。

never 型はどこに出てくるのか

たとえば loop キーワードは never 型を返します。下記はコンパイルがしっかり通ります。

fn print_forever() -> ! {
    loop {
        println!("永遠に。。。");
    }
}

その他、Rust では breakcontinuereturnpanicunimplemented マクロ*2unreachable マクロ*3などが never 型を返します。

ここまでの例では、いくぶん暗黙的な型変換の例であったり、あるいは関数の戻り値としてしか ! を登場させてきませんでした。これは意図的で、現在の stable Rust では never 型は関数の戻り値でしか指定できません。基本編では説明しませんが、のちの発展編ではそのあたりの経緯を少し説明していますので、興味がある方は発展編まで読み進めてください。

Coercing

never 型はいろいろな型に型強制(type coercing; coercing の読み方はこの動画)できます*4。型強制は要するに、ある型と、必要とする型とが異なる場合に、自動的に型の変換を挟むことです。たとえば下記の例では、!i32 に型強制(自動的な型変換)が行われています。

fn main() {
    // 最終的な型は i32 として判定される。これは、型強制が起きているためにこうなる。
    let result: i32 = match answer() {
        Ok(v) => v, // こちらの腕は `i32` を返す
        Err(err) => panic!("unexpected error") // こちらの腕は `!` を返す
    };
    println!("{}", result);
}

enum Error {}

fn answer() -> Result<i32, Error> {
    Ok(1)
}

通常、パターンマッチの各腕の型は最終的には一致している必要があります。つまり、たとえば Err(err) => panic!(...)と書いている箇所に、Err(err) => "unexpected error"と書いて &str 型にしてしまうと、この時点でコンパイルエラーになります。

fn main() {
    // 最終的な型は i32 として判定される。これは、型強制が起きているためにこうなる。
    let result: i32 = match answer() {
        Ok(v) => v, // こちらの腕は `i32` を返す
        Err(err) => "unexpected error" // こちらの腕は `&str` を返す
    };
    println!("{}", result);
}

enum Error {}

fn answer() -> Result<i32, Error> {
    Ok(1)
}
error[E0308]: `match` arms have incompatible types
 --> src/main.rs:5:21
  |
3 |       let result: i32 = match answer() {
  |  _______________________-
4 | |         Ok(v) => v, // こちらの腕は `i32` を返す
  | |                  - this is found to be of type `i32`
5 | |         Err(err) => "unexpected error" // こちらの腕は `&str` を返す
  | |                     ^^^^^^^^^^^^^^^^^^ expected `i32`, found `&str`
6 | |     };
  | |_____- `match` arms have incompatible types

型強制は、型同士のミスマッチが起きたタイミングで一度チェックされます。先ほどもおそらく一度チェックが走っているのでしょう。結果、 &stri32 には部分型等の関係性が何もなかったために変換は起きなかったというわけです。

一方で never 型は例外的で、すべての型のボトムに存在する型のため、!i32 の部分型になっています。これによって i32 への型強制が起き、結果 i32 として型が判定されます。

たまに出てくる、すべての型の基底にあってどんな型にも強制できる型という理解で、さしあたっては大丈夫です。

発展

以下は少し発展的な内容になります。

never 型はまだ安定化されていない

実は never 型はまだ型としては「本格的には」安定化されていません*5。たとえば、次のような明示的な never 型の変数における型宣言は、まだ stable コンパイラではできません。nightly を使用し、feature を有効化する必要があります。

stable コンパイラ 1.47.0 で下記コードをコンパイルしてみます。

fn main() {
    let p: ! = panic!("never type is experimental!");
}
error[E0658]: the `!` type is experimental
 --> src/main.rs:4:12
  |
4 |     let p: ! = panic!("never type is experimental!");
  |            ^
  |
  = note: see issue #35121 <https://github.com/rust-lang/rust/issues/35121> for more information

これを回避するためには、#![feature(never_type)] を使用する必要があります。下記は nightly 1.49.0 を使用してコンパイル可能でした。

#![feature(never_type)]
fn main() {
    let p: ! = panic!("never type is experimental!");
}

std::convert::Infallible

Rust 1.34.0 から導入された std::convert::Infallible という型を使用すると、「決してそこでエラーが起こらないことを示す」ことができます(できることになっています)。決してXXXが起こらない――これは never 型が本来担うべき役割なように見えますね。

use std::convert::Infallible;

fn main() {
    // このコードはコンパイルが通る。
    // loop は `!` 型であることに注意。
    let p: Infallible = loop {
        println!("never fail but print forever")
    };
}

この std::convert::Infallible の定義は実は空の enum になっています

pub enum Infallible {}

将来的にはこの Infallible は下記のように ! のタイプエイリアスとして定義される予定のようです。

pub type Infallible = !;

ただこれは、あくまでエラーがないことを示すがための型のように思います。すべての never 型を代表するような一般性はないかもしれません。

まったく異なる Infallible という型が、なぜ先ほどのように ! の代用できるかというと、「基本」で示したように裏で型強制が起きているからなのでしょう。! はボトム型である以上実質どの型にも型強制されうるので、結果表面上は Infallible = ! なように見えるというわけです。そして、Infallible は値をもたないので、実質「値を決して返さない」never 型の機能も満たせているというわけです。

例としては Result 型に対する使用でしょう。本来エラーが返ることはない操作に対して、なにか他の実装との都合でどうしても Result 型を使用する必要がある場合に、「決してエラー側は返すことがない」を表現するために Result<A, Infallible>(A は任意の型)といった宣言をして回避するという手法が取れます。将来的には Result<A, !> としたいのです。

たとえば std::convert::Infallible のドキュメントには TryFrom の例が記載されており、TryFromOk しか返さないことを Result 型を使用しつつも表現できています。

impl<T, U> TryFrom<U> for T where U: Into<T> {
    type Error = Infallible;

    fn try_from(value: U) -> Result<Self, Infallible> {
        Ok(U::into(value))  // Never returns `Err`
    }
}

普段のアプリケーションで意図的に never 型(Infallible)を使用したいというのは稀かもしれませんが、頭の片隅に置いておきたい話ですね。

で、なんで Rust には ! がいるの?

基本編でも書いたように、最終的な挙動や意味合いは unit 型(unit 型は Rust では要素0個の Tuple (())でしたね)と似ています。ではなぜ、わざわざ never 型を区別して用意しているのでしょうか?

詳しい人が StackOverflow にて回答してくれています。簡単に要約していきます。

stackoverflow.com

まずひとつめの理由は、意味のないコード片をコンパイルタイムで拾い上げられるからです。

たとえば、Rust では下記のようなコードを書くと警告が出ます。このコード片では exit が never 型を返します。println!exit より後ろにあるため、決して呼び出されないからです。これらを解析することで、下記のような警告を出せるわけです。

fn main() {
    std::process::exit(0);
    println!("unreachable");
}
warning: unreachable statement
 --> src/main.rs:3:5
  |
2 |     std::process::exit(0);
  |     --------------------- any code following this expression is unreachable
3 |     println!("unreachable");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement
  |
  = note: `#[warn(unreachable_code)]` on by default
  = note: this warning originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

Rust ではやはり他のモダンなコンパイラと同様にデータフロー解析が行われるので、そこで利用されるようです。

次は、Rust のコンパイラバックエンドとして使用されている LLVM の最適化に使用できるようです。以下は私が LLVM にまったく詳しくないので間違ったことを書くかもしれません。

never 型は上記のコンパイルエラーからもわかるとおり、到達しないコード片の解析に利用できます。同時に、 LLVM にもそのことを伝えられるようです。

先ほどのコードを LLVM IR に直してみてみると、unreachable という文字列が見られます。ドキュメントを参照してみると、unreachable という命令があります。これは、ドキュメントによるとオプティマイザにそのコード部分が使用されないことを伝える役割をもっているそうです。

; Function Attrs: nonlazybind uwtable
define internal void @_ZN10playground4main17h9866004d1365e02aE() unnamed_addr #1 !dbg !163 {
start:
; call std::process::exit
  call void @_ZN3std7process4exit17h58c2c865f4c85e96E(i32 0), !dbg !166
  unreachable, !dbg !166
}

あるいは、先ほどの例にあるように Ok しか返さない Result 型で Err 側を呼び出してしまった場合などについては、コンパイラOk のケースだけ考えればよくなるというメリットも示されています。

他の言語での never 型導入のメリットは、Rust とは少し事情が異なるかもしれません。調べてみてもおもしろいかもしれません。

Rust ではなぜ、関数の戻り値だけにしか ! を使用できなくしてあるの?

まず結論ですが、これは調べてみましたがわかりませんでした。コンパイラをもっとしっかり読んでみるべきかもしれません。読んでみてなにかわかったら追記をしようと思います。以下は調べた話を目的もなくつらつらと書きます。

なんとなくなんですが、型推論の問題があるのかなと思いました。変数の束縛時やトレイトの型引数には ! を直接宣言することは今のところ無理で、関数の戻り値のように明示的な場合はOKということは、型推論や型解決のフェーズでなにか問題があったりするということなのでしょうか。

never type は一度 revert されています。経緯に関してはこの記事がとても詳しいです。このコミットによると、まず戻されたのは stable にするためのスイッチオンの部分だけのようです。

never 型の実装の追加に関する PR はないかと少し探してみました。すると、この PR がそれに該当したようで出てきました。読んでみると、どうやら型チェック周りの実装を修正している形跡があります。そしてこの実装は、(現在のコンパイラとは大きくディレクトリ構成が違う気がするので推測になりますが)現在もコンパイラに残っているようです。つまり、スイッチさえ切り替えればオンにできる状態なのかなと思います。

えー、なんの答えにもなっていないので、私の疑問をまとめておくと:

  • 関数の戻り値と、変数の型宣言やトレイト等の型引数では、指定可能な型が異なる実装をされている?
  • なぜ、関数の戻り値には指定して OK としてあって(これは私の予想では ! は型強制を走らせられるため、最悪使う側で型を解決し、! をなくした状態で型推論に乗せられるから)、その他の箇所で自主的にユーザーが設定するのは NG としたのか(私の予想では、実装当時 !型推論アルゴリズム上なにか解決できないものが存在していたから)?
  • 実装場所どこー

です。Rust の型周りをもう少し勉強しないとわからなそうです。

*1:これは型理論の標準的な入門書とされる TaPL にも載っています。TaPL には、「Bot は空ということである。つまり、型 Bot の閉じた値は存在しない。(...)Bot が空であるからといって、役に立たないということにはならない。むしろ Bot は、ある種の操作(特に、例外の送出や継続の呼び出し)が戻ってこないはずであるという事実を表現するのに大変都合がよい方法である」(p.150)と書かれています。Bot というのはボトム型のことです。

*2:まだ実装していないことを示す際に便利なマクロです。

*3:その分岐・ブロックには決して処理が到達しないはずであることを示すマクロです。

*4:頭痛が痛いみたいな感じがしますね。

*5:んん?さっき型って言ったじゃん!と思うかもしれません(私も思いました)。いくつかのPRを見ていると、full-fledged (羽が生えきった、という意味になるんですが、日本語の文脈に直すなら、要するに完全体とかフル装備とか本格的とかそういう意味です)という単語が出てきます。現状の Rust の never 型は full-fledged ではない状態ということになります。という意味です。たしかに、関数の戻り値としては使用可能だけど、変数宣言や型エイリアス、型引数には使用できないというのは片手落ち感がありますよね。

ScalaMatsuri 2020に行ってきた! #ScalaMatsuri

2020/10/17-2020/10/18で開催されていた ScalaMatsuri に参加してきました。お忘れかもしれませんが本職は Scala エンジニアです。会社のスポンサー枠で参加しました。

聞いたセッション

スライドリンク集はこちらに。ありがとうございます。

自由、平等、ボックス化されたプリミティブ型

日本語のセッションでした。Java の歴史をさらって問題意識などを振り返りつつ、Scala の等価性の問題を知ることができました。これは Java 側の実装との根深い問題で、それをよい方向に進ませるためにこれまでいくつか修正が行われてきたようです。

dotty に、等価性に関連する新しい機能が追加されていてなんでだろうと思っていたんですが、Scala の等価性判定には不健全性が含まれていて、それに対する対応を行うために行われた機能追加だったのだということをしることができました。

eed3si9n.com

Caliban: Functional GraphQL Library for Scala

英語のセッションでした。Scala で GraphQL なライブラリ Caliban の設計思想などが紹介されていました。

ZIO がなんか最近あつい気がします。私も使ってみたい。

www.slideshare.net

Dotty ではじめるマルチステージプログラミング入門 / An introduction to multi-stage programming with Dotty

日本語のセッションでした。Dotty の新機能マルチステージプログラミング(MSP)。Dotty はこのあたりがすごく強化されていて、使うの楽しみです。

Tagless-final のところが非常に勉強になりました。私は F[_] くらいの理解しかしていませんでしたが、もともとは埋め込み DSL を構築するための手法で、研究者さんなどは便利だからこれを使うというような話がおもしろかったです。

github.com

Akka Streams vs Spark Structured Streaming

日本語のセッションでした。ちょうど Akka Streams を導入してログのストリーミングをしようとしていたのですが、Spark Structured Streaming という選択肢もあることを知りました。ただ、Spark の方は実質マイクロバッチで、使う際には注意が必要そうに感じました。

www.slideshare.net

モダンなJVMマルチスレッド / Modern JVM Multithreading

英語のセッションでした。JVM のマルチスレッドプログラミングに関するいい復習になりました。ただ、わたしは Quill は推しません。

pjurczenko.github.io

オンラインカンファレンスとしての工夫

非常に完成度の高いオンラインカンファレンスでした。運営の方にはぜひ知見をご共有いただきたいです…!

Rust.Tokyo も今年11月7日〜8日に RustFest Global を開催予定なので、オンラインカンファレンスでどのような工夫をしているのかを見ていました。

  • セッション自体は Zoom Webinar を使用して開催。
    • 各部屋にモデレータの方がいて、会の進行を管理していました。
    • セッション中のチャットは Discord にて行われていました。
  • Webinar 上にて同時通訳を配信。
    • 英語か日本語か選べて、日本語セッションときは裏で英語に同時通訳していました。Rust.Tokyo も本当はやりたい。
  • Discord をフル活用。
    • いわゆる認証を通すと、隠し Discord チャンネルが表示されるようになる。
    • スポンサーブースも Discord 上に存在。
    • チャンネルは参加者が自由に作っているようだった(会の途中で #functor なるチャンネルが作成されるなど笑)。
  • 懇親会は Discord 上にて実施。
    • 私は参加していませんが、ちらっと覗いた感じトークルームを使用して開催されていたようです。

Discord は普段友人とゲームのボイチャをするために使っていたくらいだったので、こんなに機能があったのかと衝撃を受けました(笑)。

最近の社内の Scala の状況

最後に。

個人ブログに会社のことを書いても仕方ないかもしれませんが、まとめておくと:

  • 完全に Go に押されている: 私のいる部署はもともと Scala が強い部署だったのですが、最近では新しいプロダクトが立ち上がるとなると、Go が採用されることの方が増えてきています。Go は社内での導入事例がたくさんあり、また他社でのはやりもあって多く採用されています。
  • 新人さんは Scala を難しいと感じるらしい: 「Scala がわからない」とよく言われます*1インターンを多く受け入れているのでわかるのですが、Java の経験がまったくない方が多く来ます。他のアルバイト先やインターン先は Rails を使っていますという方が多く、まず Java のインタフェースなどの概念の習得に苦戦している印象があります。次に苦戦しているのは Future か implicit。

です。

Web 系で、なおかつうちのようにプロダクトの興亡が激しい会社の場合、「そのときはやっている技術」を使ってプロダクト作った後、とくに更改することなくプロダクトが畳まれてしまうということがあります*2。この「そのときはやっている技術」に Go が入っているので、結果的に Go の採用事例が増えているようには思います。逆に、Scala 製のプロダクトが畳まれて、結果的に採用数が減っているように見えるといったところでしょうか。

新人さんを見ていての苦戦ポイントは、彼ら彼女らは Java に登場する概念に馴染みがなく、結果的にまず Java のエコシステムの習得に少し苦労しているようだという印象です。キャリアの中で Scala第一言語としてやる際に苦戦しているのはまず traitabstract class の違いや、それ以外のポイントですと Future の理解や implicit の理解もなかなかの苦戦ポイントのようです。

プロダクトでは Scala を中心に実装されているので、引き続き Scala エンジニアの採用や育成などは肝になってくるのですが、一方で社内勢力が減ってきているのも事実という現状です。さて、これからの技術戦略をどうしていこうかなというのが私が抱える課題になっています。なんとか Scala の社内での盛り上がりを作っていきたいです。

*1:わからない、をわかるように教えるのが教える側の責任なので、これは教える側に問題があります

*2:これは経営戦略の違いなので、長期間続くプロダクトを作れることとどちらが優れているという話には帰結しません。

My Rust 2021

It will be 3 years since I started to code in Rust. I had started to code 6 years ago, so half of my career have been dedicated to write code in Rust. Over recent years, I organised a Rust conference happened in Japan, and I'm now organising RustFest Global with amazing organisers (big thank you to Flaki, Jan-Erik, and other warm people. We're now working hard, stay tuned on Nov 7th-8th!).

I decided to jot down about Rust 2021. My expectations for the future of Rust are the following:

  1. Support async function in trait (and impl).
  2. Describe the difference between tokio and async-std more clear.
  3. Work on more materials for intermediate / advanced Rustaceans.

Support async function in trait (and impl)

Refs to Support `async fn` in trait methods (async/await follow-up) · Issue #2739 · rust-lang/rfcs · GitHub.

Now the Rust users try to implement the following code, we should use async-trait crate. For instance, the following one will be compile error.

trait AsyncTrait {
    async fn f() {
        println!("Couldn't compile");
    }
}

Then we will get an error:

async-trait-sandbox is 📦 v0.1.0 via 🦀 v1.44.0 on ☁️  ap-northeast-1
❯ cargo check
    Checking async-trait-sandbox v0.1.0
error[E0706]: functions in traits cannot be declared `async`
  --> src/main.rs:8:5
   |
8  |       async fn f() {
   |       ^----
   |       |
   |  _____`async` because of this
   | |
9  | |         println!("Couldn't compile");
10 | |     }
   | |_____^
   |
   = note: `async` trait functions are not currently supported
   = note: consider using the `async-trait` crate: https://crates.io/crates/async-trait

To avoid this error for a meantime, we should add a crate async-trait. Add the following line to Cargo.toml.

[dependencies]
async-trait = "0.1.36"

Add #[async_trait] attribute then we will be able to pass the strict compiler :)

use async_trait::async_trait;

#[async_trait]
trait AsyncTrait {
    async fn f() {
        println!("Could compile");
    }
}

async-trait is an awesome crate that enable us to async code being more flexible. However, I want Rust to support async fn in trait.

In developing web applications, we're used to build them with Dependency Injection (DI). When we're to use DI, interfaces and implementations are decoupled by using trait. For instance, I'm usually architecting components that fetch data from persistence layers as bellow:

#[async_trait]
pub trait TodoRepository {
    fn get(&self, id: String) -> Result<Option<Todo>, AppError>;
    fn insert(&mut self, todo: Todo) -> Result<Option<Todo>, AppError>;
    fn update(&mut self, todo: Todo) -> Result<Option<Todo>, AppError>;
    fn delete(&mut self, id: String) -> Result<(), AppError>;
}

#[async_trait]
impl TodoRepository for TodoRepositoryImpl {
    fn get(&self, id: String) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    fn insert(&mut self, todo: Todo) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    fn update(&mut self, todo: Todo) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    fn delete(&mut self, id: String) -> Result<(), AppError> {
        // implementation
    }
}

The #[async_trait] attribute needs to set both on trait and impl but that's a bit noisy. I wanna define async fn directly as below.

pub trait TodoRepository {
    async fn get(&self, id: String) -> Result<Option<Todo>, AppError>;
    async fn insert(&mut self, todo: Todo) -> Result<Option<Todo>, AppError>;
    async fn update(&mut self, todo: Todo) -> Result<Option<Todo>, AppError>;
    async fn delete(&mut self, id: String) -> Result<(), AppError>;
}

impl TodoRepository for TodoRepositoryImpl {
    async fn get(&self, id: String) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    async fn insert(&mut self, todo: Todo) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    async fn update(&mut self, todo: Todo) -> Result<Option<Todo>, AppError> {
        // implementation
    }

    async fn delete(&mut self, id: String) -> Result<(), AppError> {
        // implementation
    }
}

I feel it more natural in case I use async fn than if use the handy crate.

Describe the difference between tokio and async-std more clear

Every time I introduce Rust to my coworkers (they're web developers as well), I always feel tough to describe the difference between tokio and async-std because there seems to be no clear description or documentation regarding the difference of two runtimes. I can find related posts on Reddit, on the other hand can't find official statements.

Work on more materials for intermediate / advanced Rustaceans

Move on to the next topic. The next one is about Rust books and guides.

Now we have a lot of introduction guides thanks to the community and enthusiastic users but mostly for beginners. We have few kinda advanced guide we can find out the same in other languages as "Effective XXX".

As mentioned in the Rust survey 2019 results, those who have finished "The Rust Programming Language" and used Rust for a long time seem to hope to read advanced material.

The topic will be about (these are a homage to "Effective Python" lol):

  • Rustic Thinking (how to design Rust application with trait and struct, tackling on clone(), hacking with smart pointers, and so on)
  • Functions (introduce some useful functions)
  • Testing and debugging (mocking, integration tests and how to use gdb on Rust, and so on)
  • Speeding up our Rust application

That's just a thought but looks good doesn't it?

That's it

That's it. Good luck everyone!

x.py について

x.py とは

Rust コンパイラ向けのツールです。Python が事前にインストールされている必要があります。

Rust コンパイラのビルド

とっても簡単。下記コマンドを実行しましょう。

まずはクローンします。

git clone https://github.com/rust-lang/rust.git

ビルドに必要なツールが README に記載されているので、それらをインストールする必要があります。私は macOS でビルドしたのですが、macOS の場合はだいたいデフォルトで入っているはずです。ただ、makecmakeninja はインストールが必要かなと思います。

https://github.com/rust-lang/rust#building-on-a-unix-like-system

回線の速度にもよりますが、まずまず時間がかかるので、紅茶でも飲みながら待ちます。

cp config.toml.example config.toml
./x.py build && ./x.py install

build を開始するとまずまず時間がかかるので、新しく紅茶を作って待ちます。ちなみに、この build を成功させるためには、10-15 GB 程度の空き領域が必要になります。

ちょっとした小ネタ

ただ、逐一ビルドしていると時間がかかって仕方がないので、そのような場合には ./x.py check が有効です。これは実質 cargo check コマンドのようなもので、ちょっとしたリファクタリングコンパイル結果をチェックするなどといった用途に使用できます。

テストは ./x.py test コマンドで実行できます。

その他使うコマンドとして、./x.py fmt コマンドがあります。これも同様に cargo fmt のように Rust のコードをフォーマットするものです。Rust のプルリクエストのレビューを見ていると、たまにコミット前に fmt を実行し忘れて指摘されているものを見かけます。コミットする前に忘れずに回しておきましょう。