🥛

Scala.js 学習として scalajs-react のサンプルを読んでみた

2021/07/04に公開

東京に引っ越した@armorik83 です。

引っ越してから長らくデスクが整っていなかったので、こういった記事を書く気力が無かったんだが、やっと整ったのでその勢いで。

今回のテーマは、Scala.js 学習の一環としてscalajs-reactサンプルを読んでみたというもの。JavaScript エンジニア目線での Scala として扱う。学習ノートで進めた順序通りに記事も執筆しているため多少構造が散漫になっていることは了承いただきたい。

どこから読めばいいか

ルートはここ。

./project/Build.scala

こういうのは勘で、とりあえずビルドツールであるsbt周りから。

一個一個が何やっているかは全く追ってないが、きっと JS コンテクストでのpackage.jsonなんだろうなと推測して終わり。

.enablePlugins(ScalaJSPlugin)

きっとこの一行が重要。plugins.sbtでプラグイン宣言。

./src/main/scala/todomvc

Scala ではこういう深いところにソースを置くのが作法らしい。

Main.scala

https://github.com/tastejs/todomvc/blob/24b83cdbd7d888540640bf3b64cf0f0348aa50d3/examples/scalajs-react/src/main/scala/todomvc/Main.scala

気になるところを見ていく。

package

Main.scala#L1

package todomvc
  • 名前空間宣言
  • Java と同じくパッケージとしてまとめる

package foo {
  //
}
package foo

//

この 2 つは同じ。


package foo.bar {
  //
}
package foo.bar

//

この 2 つは同じ。


package foo {
  package bar {
    //
  }
}
package foo
package bar

//

この 2 つは同じ。こう書くものは次と同じ。(thx @xuwei_k)

package foo.bar

import foo._

たぶんpackage foo.barが一番使われるケース。

import

Main.scala#L3

import japgolly.scalajs.react._

JavaScript (ES2015)のようにimport 変数名 from 'モジュール名文字列'とはしない。import 文末尾のものが使えるようになる。ここが_のときは wildcard。今回の場合、japgolly/scalajs/reactの全てを指すようだ。

個人的な印象としては、明示できる量の時は安易に wildcard にせず、使うものだけをその都度列挙した方が親切と感じた。おそらくチーム内で合意を取ったほうがいい。

package.scalaからindex.jsみたいな雰囲気を感じる。

メソッドや定数を定義できる「パッケージオブジェクト」とは
http://www.atmarkit.co.jp/ait/articles/1205/22/news134_2.html

object Main extends JSApp

Main.scala#L12

extends JSAppしておくと、そのobjectに宣言されたmain()が JavaScript 側のエントリポイントとして呼ばれる。以前の記事で少し触れた。

main()

Main.scala#L46-L48

dom.document.getElementsByClassName("todoapp")(0)

この辺は JavaScript コンテクストを彷彿とさせるが、2 点注目すべき箇所がある。

文字列が'todoapp'ではなく"todoapp"

JavaScript では''""は等しく、文字列として HTML を書く際に属性をダブルクオートで記述するための利便から普段はシングルクオートで書くことが多かったが、Scala では明確に区別される。

Scala においてシングルクオートは Char 型リテラルであり、ダブルクオートが文字列(String 型リテラル)となる。

index 添字が[]ではなく()

java
a[0] = 123;
final int e = a[0];
scala
a(0) = 123
val e = a(0)

なるほど。なおセミコロンも不要。

React.scala

object React extends React

React.scala#L8-L9

main()を読むと、ここでReact.render()となったため実装側に移る。

object React extends React
trait React extends Object {
  //
}

という奇妙な記述が出てきたが、きっとtrait Reactobject Reactとして扱うという意味なんだろう。(それ以外にextends Reactの出元になりそうなもの無いし)

このReact.scalaだが、実装が一切ないので(そりゃそうだ、JavaScript のライブラリなんだから)じゃあこのソースは一体何をしているかというと、型定義のように見える。TypeScript でいう.d.tsだ。ということは、.d.ts.scalaに変換するやつなど誰かもう作っているだろうと思うと、やっぱりあった。クオリティは不明。

型定義なのは分かったが、これが実際の JS の何と紐付いているかはどこで取れるのだろうか。

Defining JavaScript interfaces with traits
http://www.scala-js.org/doc/calling-javascript.html

このjs.nativeのようだ。たぶん「同名前空間の同プロパティと紐付く」みたいな解釈でいいはず。

動かして確認

動かして何が格納されているか調べてみるため、fastOptJSで読める JS にコンパイルする。方法は過去の記事でまとめた。

$ sbt
> fastOptJS

およそ 47,000 行の JS が出力される。atom では開いてすぐ固まり画面がまったくスクロールしなくなったので、TextEdit.app やパフォーマンスに取り組んでいるエディタで開くといい。

todomvc-opt.js
$c_Ltodomvc_Main$.prototype.main__V = (function() {
  $g["React"]["render"](this.router$1, $g["document"]["getElementsByClassName"]("todoapp")[0])
});

およそ 12,500 行目付近にこのような出力がある。$gwindow、そしてそこにwindow.Reactとなる形で$g["React"]となっている。Scala.js コンパイル後の JS は大半の変数名が名前空間付きなので非常に長いが、長いだけで JS としては読めるレベルである。

この React は格納済みだが、Browserifynpm iもしていないのにどこから?と調べてみたら、先のBuild.scalaに答えがあった。

Build.scala
def useReactJs(scope: String = "compile"): PE =
    _.settings(
      jsDependencies += "org.webjars" % "react" % "0.12.1" % scope / "react-with-addons.js" commonJSName "React",
      skip in packageJSDependencies := false)

sbt で依存解決しているらしい。sbt ログにも次のような表示がみられる。

[info] downloading https://repo1.maven.org/maven2/org/webjars/react/0.12.1/react-0.12.1.jar

この実体はどこにダウンロードされたのか調べると、~/.ivy2/cache/org.webjars/react/jars/react-0.12.1.jarに居た。WebJars とは Maven などの Java エコシステムにおいて JS や CSS を解決するためのリポジトリらしく、有名どころはほとんどが保守されている。AngularJS もあった。

ivy はApache Ivyといい sbt が利用している依存解決の実装のことだそうだ。正直 Maven や Ivy と言われてもさっぱりだが、npm や bower のようなものと認識している。

2014 年 4 月
exe/dmg しか知らない人のためのインストール/パッケージ管理/ビルドの基礎知識 (3/4)
http://www.atmarkit.co.jp/ait/articles/1403/31/news032_3.html

ということで、このreact-0.12.1.jar内に生の JS ソースが入ってたため、これを解決している。

JSExport を CommonJS モジュールとして扱う方法もあるため、Browserify 側に寄せることも可能だろうと見ている。アプリケーションや開発リソースによっては WebJars に寄せきるのもアリかもしれない。(React 実装を全て Scala に寄せると JSX として書けないという問題が起きるので、コストと相談)

CTodoList.scala

React の挙動の詳細は本稿では割愛する。ざっくり言うと、HTML 内の<section class="todoapp"></section>にて指定の React Component が展開される。その指定された Component がここではCTodoListだ。

CTodoList.scala

非常に奇妙なのが、このソースに現れるrenderだ。

CTodoList.scala#L59-L80
def render: ReactElement = {
  val todos           = $.state.todos
  val filteredTodos   = todos filter $.props.currentFilter.accepts

  val activeCount     = todos count TodoFilter.Active.accepts
  val completedCount  = todos.length - activeCount

  <.div(
    <.h1("todos"),
    <.header(
      ^.className := "header",
      <.input(
        ^.className     := "new-todo",
        ^.placeholder   := "What needs to be done?",
        ^.onKeyDown  ~~>? handleNewTodoKeyDown _,
        ^.autoFocus     := true
      )
    ),
    todos.nonEmpty ?= todoList(filteredTodos, activeCount),
    todos.nonEmpty ?= footer(activeCount, completedCount)
  )
}

React を理解していると、これが JSX に相当することは察しがつくがそれにしても奇妙な面をしている。1 つずつ見ていく。

<.div

JSX の<div>のように見えるが、そもそもここでいう<は何か。追ってみると scalajs-react 側に実装があった。

package.scala#L33-L45

package.scala#L33-L45
object prefix_<^ extends Base {
  @inline def < = Tags
  @inline def ^ = Attrs
}

object svg {
  object all extends Base with SvgTags with SvgAttrs

  object prefix_<^ extends Base {
    @inline def < = SvgTags
    @inline def ^ = SvgAttrs
  }
}

<はここで宣言されている。下には^もいた。想像通り、タグや属性のためのものだ。JavaScript では変数名に使用できる記号は$_に限られているが、Scala はけっこうなんでもありだ。

Scala 用メソッド名の変換
http://www.ne.jp/asahi/hishidama/home/tech/scala/reflect.html#h_NameTransformer

~~>?

これまた奇妙だ。これはScalazReact.scala#L25-L39にある。

ScalazReact.scala#L25-L39
@inline implicit final class SzRExt_Attr(private val _a: Attr) extends AnyVal {

  @inline final def ~~>(io: => IO[Unit]): TagMod =
    _a --> io.unsafePerformIO()

  @inline final def ~~>[N <: dom.Node, E <: SyntheticEvent[N]](eventHandler: E => IO[Unit]): TagMod =
    _a.==>[N, E](eventHandler(_).unsafePerformIO())

  @inline final def ~~>?[T[_]](t: => T[IO[Unit]])(implicit o: Optional[T]): TagMod =
    _a --> o.foreach(t)(_.unsafePerformIO()) // This implementation keeps the argument lazy
    //o.fold[IO[Unit], TagMod](t, ~~>(_), EmptyTag)

  @inline final def ~~>?[T[_], N <: dom.Node, E <: SyntheticEvent[N]](eventHandler: E => T[IO[Unit]])(implicit o: Optional[T]): TagMod =
    _a.==>[N, E](e => o.foreach(eventHandler(e))(_.unsafePerformIO()))
}

ここでメソッドとして宣言されているが、使うときは^.onKeyDown ~~>? handleNewTodoKeyDown _,のように扱われる。これを中置記法という。

@inline

inlineについては、はっきりと分からなかったのだが、おそらく C 言語などのインライン展開を意味するのではないだろうか。なお@は Scala ではAnnotationsと呼ばれる。一方 JavaScript の次期仕様としては、Decorators なるものが提案されている

:=

これについては悩んだ。sbt にも Scala.js にも登場するためどちらを使っているか追いきれなかったが、おそらく Scala.js 側のdefだろう。

TreeDSL.scala#L48-L49

今回はここまで

Scala と出力 JS 読みながら、実際に動かしながら、それぞれ見ていった。気になったところは順次潰していったが、まだまだ興味は尽きない。implicitcase classなどは今後に持ち越す。

最後に実際に動いているものを。

http://todomvc.com/examples/scalajs-react/#/

「ああ…」としか言えん。

以上。

Discussion