🥰

Rustacean のための Scala 3 入門

2022/12/23に公開約12,000字

この記事は Rust アドベントカレンダー 2022 12/16 の、Scala アドベントカレンダー 2022
12/17 の記事です.

Rust 流行ってますね. Rust は元々はシステムプログラミング言語らしいですが、フロントエンドのツール開発者が盛んに使っていることから JSer やフロントエンドエンジニアも Rust を触ったことがある人が増えている印象があります.

JavaScript や Ruby のようなゆるゆる動的型付け言語の世界から Rust に触れて、コンパイラがコードをしっかりと検証してくれる快適さにキャッキャっしている人も多いのではないでしょうか.

一方で、 Rust のライフタイムはメモリについて考えないといけないので難しいです. そんなあなたに朗報が.
GC がある Rust こと Scala を使ってみませんか? Scala 3 は Rust を書いているのとほとんど同じ感覚でプログラムを書くことができるみたいですよ?

Why Scala (3)?

  1. Rust と同様に強い静的型付け言語なのでコンパイル時にコードを検証できます.
    • union 型 A | B , and 型 A & B やシングルトン型 type LogLevel = "INFO" | "WARN" | "ERROR" も使えます.
  2. Rust と同じように不変の値と変数を区別しています. シンタックスも似ていてパターンマッチ、trait や derive が使えます.
  3. Rust と違ってガベージコレクションがあるのでライフタイムを気にせずにコードを書くことができます.
  4. リッチな Repl(https://ammonite.io) や worksheet のようにインタラクティブにコードを書けるツールが充実しています.
  5. Scala.js・Scala Native を使って、JavaScript や ネイティブにクロスコンパイルすることができます. Write Once. Run Anywhere
  6. 豊富なライブラリ. Java のライブラリも全て使えます. scaladex から Scala のライブラリを検索できます.
  7. 関数型プログラミング. Kotlin の arrow-kt や TypeScript の fp-ts と違って Either や Option 型が標準で入っています. 型クラス(trait)や for comprehension の機能もあります.
  8. サードパーティーの catscats-collectionscats-effect を使えばもっと関数型テイストの強いプログラムを書けます.
  9. 言語標準のメタプログラミングでコンパイル時の AST 変換を普段のプログラムを書くのと同じような感覚で書けます.
  10. and more...
Rust Scala
ビルドツール cargo + build.rs sbt, mill
開発環境 vscode +rust analyzer vs code/neovim + metals, IntelliJ + Scala plugin
linter, formatter clippy, rustfmt scalafix, scalafmt
セットアップツール rustup coursier, scala-cli
メモリ管理 lifetime GC
不変・可変 let, let mut で区別 val, var で区別. 標準ライブラリのコレクションも immutable と mutable に分かれている.
文法 直積型 struct/ 直和型 enum・パターンマッチ・derives などのマクロ 直積型 case class/直和型 enum(sealed trait)・パターンマッチ・末尾再帰・derives などのマクロ
非同期ランタイム tokio, async-std Future, cats-effect IO, ZIO
関数型プログラミング ??? cats, cats-effect, ZIO, scalaz
フロントエンド wasm? Scala.js
Repl Evcxr scala標準の repl,ammonite,scala-cli

Rust - Scala Getting Started

開発環境

Scala を使い始める人の最初のハードルを下げるために開発されている scala-cli というコマンドラインツールがあります. ファイル一つでプログラムを書けるのでとてもおすすめです.

複雑なビルドが必要になったらビルドツールを使いましょう.

cargo に相当するビルドツールは sbtmill があります.

迷ったらデファクトの sbt がおすすめです. cargo は cargo.toml ファイルでビルドを定義しますが、sbt は build.sbt というファイルに Scala の DSL でビルド定義を書きます.

例えば src/main/scala 以下に Scala 3.2.1 でコードを書く場合は次のように build.sbt を書きます.

build.sbt
lazy val lib = project.in(file("."))
  .settings(
    scalaVersion := "3.2.1"
  )

IntelliJ ユーザーなら Scala プラグインを使うとシュッと開発環境を用意できます.
VS Code ユーザーや軽量なエディターが好みであれば Rust Analyzer に相当する Metals という拡張機能を入れましょう.

文法

次に基本的な文法を比較してみましょう.

変数定義

let a = todo!();
let mut a = todo!();
// workaround reserved word
let r#type = todo!(); 

Scala では不変な値は val、可変な値は var で定義します. 予約語は バックティックで括れば使えます.

val a = ???
var a = ???
// workaround reserved word
val `type` = ???

文字列の interpolation

let (a,b) = (1,41);
format!("{} + {} = {}",a,b,a+b)

" の前に s をつけます. 内部的には StringContext が使われています. これを応用するとコンパイル時に json を検証する json"""{"key": "value"}""" のような機能を作ることもできます.

https://zenn.dev/110416/articles/334ef1c6255588

val (a,b) = (1,41)
s"$a + $b = ${a + b}"

構造体

struct A {
  field1: String,
  field2: i32
}

impl A {
  pub fn new(field1:&str,field2:i32) -> Self {
    Self {
      field1,
      field2
    }
  }
  pub fn name(&self) -> String {
    self.field1.to_string()
  }
}

Scala で構造体のようにパターンマッチ可能な型を定義する際は case class を使います.

※ この caseswitch の分岐で使われる case というよりも データを入れる "箱" のニュアンスの case です.

case class A(field1: String, field2: Int){
  def name: String = field1.toString()
}

object A:
  def apply(field1: String, field2: Int) =
    A(field1,field2)

enum

enum X {
  A{f1: i32,f2: String}
  B{f1: String}
}

impl X {
  fn name(&self) -> String {
    match self {
      A(f1,f2) => f1.to_string(),
      B(f1) => f1.to_string()
    }
  }
}

enum の定義はほとんど Rust と同じです. メソッドを生やす場合は、 extension を利用します. enum にももちろんパターンマッチできます.

enum X:
  case A(f1:Int,f2:String)
  case B(f1: String)
object X:
  extension (x:X)
    def name: String = x match
      case A(f1,_) => f1
      case B(f1) => f1

ちなみに Scala の enum は以下のコードのショートハンドです.
sealed はその型のサブクラスがそのファイル内でしか定義されていないことを保証するのでパターンマッチで exhaustive check が効きます.

sealed trait X

object X:
  case class A(f1:Int,f2:String) extends X
  case class B(f1) extends X
def program =
  val x: X = ???

  x match
    case A(f1, f2) => ???
    // warning: missing match arm

Rust と異なり、再帰的なデータ構造も容易に定義できます.

enum Tree:
    case Node(l: Tree,r: Tree)
    case Leaf(i: Int)

データ構造

Scala のデータ構造、特にコレクションは immutable と mutable に分かれています.

次のように scala.collection のネームスペースからインポートできます.

import scala.collection.mutable.<collection name>
import scala.collection.immutable.<collection name>

Rust の iter のように map, filter, fold, flatMap(≒ and_then) などのコンビネーターが生えています.

コレクションの性能特性は公式サイトにまとまっています.

https://docs.scala-lang.org/ja/overviews/collections/performance-characteristics.html

関数

Rust では Box<dyn Fn> などと書かないといけないですが、Scala では関数を (arg: 型) => 返り値 と書けます.

val f = (a: Int) => ???
// または
val f: Int => Nothing = a => ???

高階関数

関数は変数に代入することも、メソッドの引数に渡すこともできます.

// 関数を受け取るメソッド
def f(g: Int => String): Int =
  val s = g(1)
  s.length()

// 関数を受け取る関数
val f :(Int => String) => Int = g => g(1).length()

f(i => (i*100).toString())
// res: Int = 3

// 関数を返す関数(メソッド)
val h: () => (Int => String) =
  () => (i:Int) => i.toString()
// 関数を返すメソッド
def h(): Int => String =
  (i:Int) => i.toString()
h()
// res: Int = String
h()(1)
// res: String = "1"

関数の合成

関数は composeandThen を使って合成できます.

val f: Int => String = i => i.toString()
val g : String => Int = s => s.length()

val `g ∘ f`: Int => Int = g compose f
val fAndThenG: Int => Int = f andThen g

メソッド

fn f(arg:&str) -> String {
  arg.to_string()
}

Rust の fn に相当するメソッドは次のように書けます.

def f(arg:String): String =
  arg.toString()

pub(crate) のような可視性は private 修飾子や protected 修飾子でコントロールできます.

private[mypkg-name] (args:String): String = ???

Generics

fn f<T,S>(a:T) -> S {        
  todo!()
}

fn f<N:Add>(n:N,m:N) -> N {
  n + m
}

ジェネリクス、あるいは型パラメーターは[] で括ります. 型に関する制約はトレイト境界 T: ATrait やサブタイピング T <: Parent で表現できます.

def f[T,S](a:T): S =
  ???
def f[N:Numeric](n:N,m:N):N = summon[Numeric[N]].add(n,m)
def f(A <: Animal)(a:A):A = ???

trait

trait Show {
    fn show(&self) -> String;
}
impl Show for String {
    fn show(&self) -> String {
        self.to_string()
    }
}
fn program() {
  let shown = "example".to_string().show();
}

Scala の trait は Java のようにインターフェースを定義するための使い方と、 Rust の trait に近い使い方があります. givenimpl に対応するイメージで書くといいでしょう.

trait[T] Show[T]:
  def show(t:T): String

extension[T:Show](t:T)
  def show(): String = summon[Show[T]].show(t)


given Show[String] with
  def show(t:String): String = t.toString()

def program() =
  val shown = "example".show()

パターンマッチ

let list = vec!["This","is","an","example"];
match &list[..] {
  [a,..] if a == "This" => todo!(),
  // ["This",..] => todo!() とも書ける
  [binding @ a,..] => todo!(),
  [] => todo!(),
}

struct A {
  field1: i32,
  field2: String,
  field3: f32
}

let a = A {field1: 1,field2: "a".into(),field3: 1.0};
let A{field2,..} = a;

let a = A {field1: 1,field2: "a".into(),field3: 1.0};  
match a {
  A {field1,field2,field3} => todo!()
}

Scala でも同様にパターンマッチが可能です. コレクション(List, Seq)、タプル、構造体(case class/enum) だけでなく文字列や正規表現にもマッチができます. ちなみにマッチの arm の実体は PartialFunction です.

https://gist.github.com/takezoe/29ebbd5575b6fcdded3b

val list:List[String] = List("This","is","an","example")
list match
  case head :: tail if head == "This" => ???
  // case "This" :: tail => ??? とも書ける
  case binding @ head :: tail => ???
  case Nil => ???

case class A(f1:Int,f2:String,f3: Double)
val a = A(1,"a",1.0)
val A(_,f2,_) = a
a match
  case A(f1,f2,f3) => ???

val x:X = A(1,"a")
x match
  case A(f1,f2) => ???
  case B(f1) => ???

有名なライブラリ

Rust も Scala もアプリケーションを書く場合、標準ライブラリだけでは少し厳しいのでライブラリを使うことになります.
web アプリケーションを作る場合は以下のようなライブラリを使うことになると思います.

目的 Rust Scala
Json・YAML などのシリアライゼーション serde (+ serde-json), serde-yaml circe, circe-yaml
http tower Java 標準、Netty や http4s
http client reqwest 同期処理で問題なく easy 重視なら request-scala, 非同期 IO が必要なら http4s
gRPC tonic scalapb
rest + openapi doc gen utopia tapir
非同期ランタイム tokio, async-std Future,cats-effect IO, ZIO
test - ユニットテストは munit, Property-Based Test は scalacheck などが使える
db アクセス sqlx Quill, doobie, slick, etc.

これらを全て比較するのは厳しいので Json に絞って比較します.

Json

Rust と Scala でそれぞれ Json パースをするコードを書いてみました.

Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
src/main.rs
use serde::{Serialize,Deserialize};

#[derive(Serialize,Deserialize)]
struct Payload {
  id: String,
  body: Body
}

#[derive(Serialize,Deserialize)]
struct Body {
  id: Int,
  name: String
}

fn main() {
  let payload = r#"{
    "id": "xxxxxxxx",
    "body": {
        "id": 42,
        "name": "john doe"
    }
  }"#;
  let model: Payload = serde_json::from_str(payload);
  let jsonBack = serde_json::to_string(&model)?;
  println!("{jsonBack:?}");
}

Scala のライブラリも Rust の crate の features のように複数のサブパッケージに切られていることがしばしばあります. json のパースや Encoder/Decoder の derive のために core に加えて parser と generic を import します.

以下のファイルは scala-cli json-handling.scala で実行できます. また scala-cli export --sbt json-handling.scala [--js|--native] で sbt. プロジェクトに export できます.

json-handling.scala
//> using scala "3.2.1"
//> using lib "io.circe::circe-core:0.15.0-M1"
//> using lib "io.circe::circe-generic:0.15.0-M1"
//> using lib "io.circe::circe-parser:0.15.0-M1"

import io.circe._
import io.circe.syntax._
import io.circe.parser._
import io.circe.generic.semiauto._

case class Payload(id:String,body:Body) derives Decoder, Encoder.AsObject
case class Body(id:Int,name:String)

@main def run() = 
  val payload = """
    |{
    |  "id": "xxxxxxxx",
    |  "body": {
    |      "id": 42,
    |      "name": "john doe"
    |  }
    |}""".stripMargin.trim
  for
    json <- parse(payload)
    model <- json.as[Payload]
    jsonBack = json.asJson
  yield println(jsonBack)

Rust の derive と違って、Scala の例ではトップレベルの型に derives をつけることでその要素の型についても再帰的に trait の実装を自動導出できます. また import io.circe.generic.semiauto._import io.circe.generic.auto._ にすると、 derives を省略しても必要に応じてトレイトが derive されます. 
なんでこんなことができるかって? そう、Scala ですから!

  for
    json <- parse(payload)
    model <- json.as[Payload]
    jsonBack = model.asJson
  yield println(jsonBack)

Either 型(Result型に相当) で fail-fast に処理を抜けるには for 式を使います.

まとめ

Rust ライクな文法、しっかりとした静的型付け、強力なパターンマッチ、豊富なデータ構造、マクロや関数型プログラミングが GC のおかげで脳死でかける気持ちよさが読者諸君にざっくりでも伝わっていたら幸いです.

日本語であれば ScalaText, Scalapedia 、英語であれば Tour of Scala、関数型プログラミングに関しては cats や cats effect のドキュメントがわかりやすいです.
Rock the JVM という YouTube チャンネルでも Scala のチュートリアルがあります.

2023 年は Scala をはじめてみませんか...?

Discussion

ログインするとコメントできます