Magnolia Tech

techっぽいことを書くブログです

Scalafmtのコメントスタイルについて調べた

こんな事を教えていただきました。

なるほど…

Scalafmtとは?

  • ScalafmtはScala用のコードフォーマッタ
  • sbt-scalafmtというsbt pluginが用意されている
  • IntelliJ IDEA用のpluginも用意されている
  • 保存時に自動的にフォーマッタをかけることが可能
  • .scalafmt.confというファイルでカスタマイズ可能
  • 同様のフォーマッタにscalariformというのも有る https://github.com/sbt/sbt-scalariform

Scaladocのスタイルの現状について

  • Scaladocの記法には3種類あることが公式ドキュメントの冒頭に書かれている

    Scaladoc | Scala Documentation

  • アスタリスクの位置と、最初の行をコメントの1行目に書くか、2行目から書くかの違い

  • 特にどのスタイルを強制する、といったことはscaladocとしてはしない
  • Scalafmtのドキュメントでは、Javadocスタイルと、Scaladocスタイルの2種類と記載

    Configuration · Scalafmt

  • JavadocスタイルとScaladocスタイルの使い分けが公式ドキュメントと一致していない

  • 冒頭に紹介されたコミットの内容がドキュメントに未だに反映されていないため
  • issueは挙がっているが、合意には至っていない

    github.com

  • 現状のScalaのライブラリは3番目のスタイルで書かれている…変えていない?

    scala/List.scala at 2.13.x · scala/scala · GitHub

終わりに

色々とカスタマイズができるscalafmtですが、最近だとgofmtのように一切カスタマイズを許容しない、という姿勢の方が分かりやすくていいですね…コードスタイルで議論したくないですよね…

以上、Scaladocのコードスタイルについて調べたことでした。

ScalateのScala 2.13.0非互換への対応を考える

スキルアップのためには、知らないこと、分からないことへチャレンジした方がいいですね。例えば、すぐに答えられそうか分からない質問をわざと受ける、というのも1つの方法です。

ということをツイートしていたら…

質問…というよりガチな依頼が来ましたので、これはこれで対応するしましょう(笑)。OSSへの貢献は、ソフトウェア開発を学ぶ上で必要な要素がたくさん詰まっていますからね。

対象のコードは何か?

元々のコードの意図を探る

  • ScalateではテンプレートをScalaのコードとしてコンパイルする方法を採用
  • コンパイルの際に発生したエラーや警告をConsoleReporterを継承したLoggingReporterを使って独自にエラーを蓄積
  • 最終的にエラーが有ればCompilerExceptionを送出
  • エラーの蓄積のために、printMessageというメソッドをオーバーライド

    (loggerという割にはログ出力は行っていないのは謎ですが…)

クラス構造の変更点を把握する

  • Scala 2.13.0-M5よりConsoleReporterが大幅にリファクタリング
  • クラス構造も変わり、ConsoleReporterに加えて、DisplayReporterというクラスが追加
  • ConsoleReporterとDisplayReporterは、PrintReporterというtraitをmix-in
  • PrintReporterがprintMessageを持つ構成となっているが、privateメソッド化されている
  • printMessageはdisplayメソッドから呼び出される点は変わらず

変更案

  • printMessageではなく、displayメソッドを継承
  • printMessageの中で行っているメッセージ文字列の組み立てをログ出力用にコピペ
  • 最後にエラーの件数を出力するためのprintSummaryメソッドが無くなったので、同等の機能を提供するfinishへの置き換え

github.com

printMessageの中で行っているメッセージ文字列の組み立てをコピペしているのでdryではないので、別の方法が有れば採用したいところですが、一旦素案レベルではできました。

ただし、まだScala 2.13.0-M5に対応したScalatestがリリースされていない関係で、この先のコンパイルでエラーが出るし、テストもできない状態なので、ここで一旦検討は終了です。

ここまで記録を残しておけば、scalatestが動くようになってから検討を再開できるかな。ちょっと面倒ですが、1つ1つ調べたこと、考えたことを記録しておかないとすぐ忘れちゃいますね。

終わりに

今回、お題を頂いた瀬良さんが執筆に参加されたScalaの入門本「 実践Scala入門」が発売されるそうです。これを読んでみんなもScalaOSS開発に参加しよう!!

実践Scala入門

実践Scala入門

「ドメイン駆動設計」を読んだ〜第1章 知識をかみ砕く〜

引き続き「ドメイン駆動設計」を読み進めました。

「第1章 知識をかみ砕く」には、ドメインエキスパートと、開発者の会話を通じてモデルを作り上げていく様子から始まります。

  • モデルを書いて可視化すること
  • 繰り返し相互のフィードバックでモデルを成長させること
  • モデルと実装(プロトタイプ)を結びつけて、早期のフィードバックを得ることの重要性
  • 新しい概念を新しいモデルの要素として、抽出・分離すること

といったことが書かれています。

モデル化そのもののテクニックの詳細は書かれていませんが、最初から全ての要素を全て揃えて完全なモデルを書くのではなく、その時点で関心を持っていること(つまり、ドメイン)に集中してモデル化し、(画面等も無いミニマムな)プロトタイピングによりその本質を捉えていることの検証とフィードバックを繰り返しているところが特徴です。

途中、ウォーターフォールにはフィードバックが欠けているために上手くいかないと記載されていますが、上手く行っているウォーターフォールも当然世の中には有るはずで、きっとそのようなプロジェクトでは意識的か、無意識かは別として段階的なモデル化とフィードバックが行われていて、反対に上手く行っていないプロジェクトではプロジェクトルールで規定された成果物を揃えることが目的になっているのではないでしょうか。

最初からすべてをあまねくモデル化しようとしても検証もできないですし、よりよいモデルのためにフィードバックを反映する余地が有りません。

開発者が考える拡張性や保守性は未来を向いていますが、ドメインエキスパートが考える拡張性は過去との連続性に主眼が置かれていて、それらはソフトウェアを作り上げる、という意味では本来相互に補完する関係にあるのではないでしょうか。

本書では詳細は触れられていませんが、フィードバックできる粒度を維持するためにも、実はドメイン駆動設計で最も大事なスキルは「今やらないこと・着目しないことを決める」では?と感じました。

エリック・エヴァンスのドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計

実践ドメイン駆動設計 (Object Oriented SELECTION)

実践ドメイン駆動設計 (Object Oriented SELECTION)

「ドメイン駆動設計」を読んだ〜第一部 ドメインモデルを機能させる〜

ドメイン」を辞書で調べると以下のような意味が書かれています。

(活動・関心・知識などの)分野, 領域, 範囲.

ドメイン駆動設計」の冒頭、「第一部 ドメインモデルを機能させる」を読んで以下のように理解しました。

  • ドメインは、関心の領域

  • モデルは、自分の関心領域を抽象化することにより、集中すべきところに集中するために作成する

  • ドメインモデルは、特定の図を示すものではなく、考え方を示すもの

  • ドメイン駆動設計では、モデルと実装の連続性を重要視している

  • 現代的なソフトウェアは複雑であり、その複雑さを適切にコントロールするためには、何らかの構造を作り出す必要が有る

  • 1つの考え方として、ドメイン駆動設計がある

抽象化により関心量を適切に配分し、全体としての複雑さをコントロールするっていうことなので、いかに「今、関心を持つべきではないこと」に関心を置かないか?そのためにはどうするか?というところも合意を図っておかないといけないな、と感じました。

人によって、いきなり実装詳細だけにに関心を持ちすぎたり、実装時の複雑さを無視した「絵に描いた餅」になってしまう危険性について常に留意する必要がありますね。

その辺のアンチパターンもまとまっていると良いなと思いました。

エリック・エヴァンスのドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計

実践ドメイン駆動設計 (Object Oriented SELECTION)

実践ドメイン駆動設計 (Object Oriented SELECTION)

RT-AC86Uを買った

特にゲーミングルータが必要なシビアな使い方をしているわけじゃないけど、アクセスポイントを増やす時にメッシュネットワークが組める、ということでこの機種を選定。

www.asus.com

セットアップ時に一時的に利用するアクセスポイントを用意して、セットアップが完了したら(SSIDとパスワード)、そのアクセスポイントが無効になる、という仕組みが良かった。

面倒くさがってデフォルトのSSIDをそのまま使うと、住宅街だとアクセスポイントがたくさん表示されてどれがどれだか分からなくなっちゃうことも有るので、最初から自分で設定する方がいいですね。

反面、ネットワーク名は近所に筒抜けなので、変な名前をつけないようにしないと。

特に色々なセキュリティ機能は試していないけど、電波の届く範囲も割と広めで、通常使う分には全然OKですね。

アクセスポイントを増やす時には、これを買う予定。

ScalaのmapValuesの挙動が2.13.0から変わっているので実装を調べてみた

scalaのmapValuesは、Mapのvalueだけにmapを適用したい時に使用するメソッドです(Mapとmapでちょっと分かりづらいですが)。

Scala 2.12.6で実行すると、以下のような結果になります。

scala> val characters = Map("Gandalf" -> "wizard", "Aragorn" -> "ranger")
characters: scala.collection.immutable.Map[String,String] = Map(Gandalf -> wizard, Aragorn -> ranger)

scala> val upper = characters.mapValues( x => x.toUpperCase )
upper: scala.collection.immutable.Map[String,String] = Map(Gandalf -> WIZARD, Aragorn -> RANGER)

まだ正式リリースされていないScala 2.13.0ですが、先日リリースされた2.13.0-M5という開発途中のバージョンで実行すると、以下のような結果になりました。

scala> val characters = Map("Gandalf" -> "wizard", "Aragorn" -> "ranger")
characters: scala.collection.immutable.Map[String,String] = Map(Gandalf -> wizard, Aragorn -> ranger)

scala> val upper = characters.mapValues( x => x.toUpperCase )
warning: there was one deprecation warning (since 2.13.0); for details, enable `:setting -deprecation' or `:replay -deprecation'
upper: scala.collection.MapView[String,String] = <function1>

何か明らかに挙動が変わっています。

何が変わったのか?

ふとこの挙動の変化についてツイートしたところ、@xuwei_kさんに丁寧に教えていただきました。

Scala 2.12.6における実装は以下の箇所の場所です(これも教えてもらった…ありがとうございます)。

github.com

mapValuesはMapを返すように定義されていますが、実態はMappedValuesという型のインスタンスを返しています。この時点ではインスタンスが作られるだけで実はmapが実行されていません。valuesを取り出すまで(getが実行されるまで)mapValuesの引数に渡された変換関数は実行されていない、つまり遅延評価されるということです。

この挙動が型から分からない、ということが課題になった、ということですね。確かにscaladocにも書かれていないし、型は普通のMapだし、全然分からないですね。

Scala 2.13.0-M5での実装

github.com

MapView.MapValuesという型のインスタンスを返しています。

MapViewの実装は以下の通りになっています。

github.com

先ほどの結果にキーを渡すと、変換されたvalueが得られました。

scala> upper("Gandalf")
res4: String = WIZARD

この変更はmapValuesだけでなく、filterにも適用されています。filterなんかはよく使われていると思うので、けっこう影響が大きいかもしれないですね。Scala 2.13.0へのアップデートの時には気をつけましょう。

Scala 2.13.0へアップグレードするとき

先ほどのmapValuesの件以外にも、網羅的にまとまっているマイグレーションガイドが有りますので、来たるべきScala 2.13.0リリースに向けて、こちらを見ておくと良いでしょう。

https://confadmin.trifork.com/dl/2018/GOTO_Berlin/Migrating_to_Scala_2.13.pdf

小さなPRを書いてOSS開発に貢献する

OSS開発に参加してみたいですよね!(断定)

でもどこから手をつけて良いか分からないことも多いですね。

そこでScalaのjson4sというライブラリをベースに、小さなPRを書いて、送るまでの流れを書いてみました。

参考にしてみて下さい。

今回は非推奨になったメソッドの置き換えを3カ所(3行)に対して行いました。とても短いPRですが、これで確実にJson4sをコンパイルする度に出ていた非推奨メソッドの警告メッセージが出なくなり、また今後のメンテナンス性も向上しました(非推奨になったメソッドはいつ削除されるか分からないので)。

GitHubからソースコードをcloneする

まずは(あとでPRを送るため)GitHub上で元のリポジトリをForkします。

GitHubの画面の右上にあるForkボタンを押してみてください。

自分のリポジトリにForkできたら、今度はそこから手元のローカル環境にcloneします。自分のアカウント(magnolia-k)では、以下のようになります。自分のアカウントに合わせて書き換えてください。

$ git clone git@github.com:magnolia-k/json4s.git

compileを実行する

sbtを起動して、compileを実行します。

(詳しいScalaでの開発方法は割愛します。その辺りは、Scalaの入門書などを参照して下さい)

すると、以下のようなメッセージが表示されました(のちほど出てくるPRがマージされると…残念ながらもう出なくなりますが)。

[warn] /home/xxx/json4s/jackson/src/main/scala/org/json4s/jackson/JValueDeserializer.scala:50:28: method mappingException in class DeserializationContext is deprecated: see corresponding Javadoc for more information.
[warn]       case _ => throw ctxt.mappingException(classOf[JValue])
[warn]                            ^
[warn] /home/xxx/json4s/jackson/src/main/scala/org/json4s/jackson/JValueDeserializer.scala:53:61: method mappingException in class DeserializationContext is deprecated: see corresponding Javadoc for more information.
[warn]     if (!klass.isAssignableFrom(value.getClass)) throw ctxt.mappingException(klass)
[warn]                                                             ^
[warn] /home/xxx/json4s/jackson/src/main/scala/org/json4s/jackson/JsonMethods.scala:20:25: method reader in class ObjectMapper is deprecated: see corresponding Javadoc for more information.
[warn]     var reader = mapper.reader(classOf[JValue])
[warn]                         ^
[warn] three warnings found
[info] Done compiling.

警告メッセージの意味を理解する

では実際に出た警告メッセージの理解し、対処方法を探って行きましょう。

順番が前後しますが、まずは下記のメッセージから…

method reader in class ObjectMapper is deprecated: see corresponding Javadoc for more information.

「see corresponding Javadoc for more information.」って書かれていて、具体的な場所を示してくれよ!って一瞬思いますが、そんな気持ちをぐっと堪えて、javadocを探します。

ObjectMapperJson4sが利用しているJackson-databindというJsonライブラリが持つclassです。

不要な箇所を削除しているので、完全に同じソースコードでは有りませんが…mapper.reader(classOf[JValue])readerメソッドの呼び出し箇所で警告が出ていて、そのreaderメソッドがObjectMapperというclassのメソッドであることが分かるでしょう(mapperの中身が_defaultMapperメソッドの中で生成されたObjectMapper)。

import com.fasterxml.jackson.databind._

trait JsonMethods extends org.json4s.JsonMethods[JValue] {

  private[this] lazy val _defaultMapper = {
    val m = new ObjectMapper()
    m.registerModule(new Json4sScalaModule)

    m
  }
  def mapper = _defaultMapper

  def parse(in: JsonInput, useBigDecimalForDouble: Boolean = false, useBigIntForLong: Boolean = true): JValue = {
    var reader = mapper.reader(classOf[JValue])

readerメソッドのjavadocは下記のURLで参照できます。

ObjectMapper (jackson-databind 2.9.0 API)

javadocには以下のように書かれています。

ObjectReader reader(Class<?> type)
Deprecated. 
Since 2.5, use readerFor(Class) instead

つまり、readerForというメソッドに置き換えれば良いことが分かります。

ちなみに今回は、結果的にreaderForで単純に置き換えれば良かったのですが、そもそも「なぜdeprecatedになったのか?」という理由を探すことができませんでした。急にdeprecatedになったので書き換えろ、と言われても…という気持ちになったので、READMEやChangesに書いておいて欲しいな、と思いました。

そもそも設計思想が変わった場合は当然単純な置き換えではいけないので…

次は同じメッセージが2つ出ている警告の方に取りかかります。

method mappingException in class DeserializationContext is deprecated: see corresponding Javadoc for more information.

こちらも

DeserializationContext (jackson-databind 2.9.0 API)

以下のように書かれているので、handleUnexpectedTokenで置き換えれば良いことが分かります。

@Deprecated
public JsonMappingException mappingException(Class<?> targetClass)
Deprecated. Since 2.8 use handleUnexpectedToken(Class, JsonParser) instead
Helper method for constructing generic mapping exception for specified type

しかし、よく見ると元のメソッドと戻りの型が違います。handleUnexpectedTokenObject型を返すのに対して、元のmappingExceptionJsonMappingException型を返しています。当然そのまま置き換えるとコンパイルエラーになります。

handleUnexpectedTokenメソッドのソースを追いかけてみましょう。

public Object handleUnexpectedToken(Class<?> instClass, JsonParser p)
        throws IOException
{
    return handleUnexpectedToken(instClass, p.currentToken(), p, null);
}

public Object handleUnexpectedToken(Class<?> instClass, JsonToken t,
            JsonParser p, String msg, Object... msgArgs)
        throws IOException
{
...
reportInputMismatch(instClass, msg);
return null; // never gets here
}

public <T> T reportInputMismatch(Class<?> targetType,
        String msg, Object... msgArgs) throws JsonMappingException
{
...
    msg = _format(msg, msgArgs);
    throw MismatchedInputException.from(getParser(), targetType, msg);
}

どうやら最終的に例外を送出するコードにしか到達せず、Object型の返り値は便宜的に付けられていることが分かりました。つまり元のコードの呼び出し方法を変えれば良さそうなことが分かります。

元々は例外オブジェクトを返り値で受けて、それをthrowしていました。

case _ => throw ctxt.mappingException(classOf[JValue])

handleUnexpectedTokenメソッドの中でthrowしているので、呼び出し時のthrowを削除します。

case _ => ctxt.handleUnexpectedToken(classOf[JValue], jp)

コミットする

コミットメッセージは、gitのお作法に従って、変更理由を書きましょう。

この記事なんか参考になると思います。

qiita.com

でもtypoの修正とかなら、「fixed typo」だけでも充分です。

PRを送る

修正ができたらPRを送ります。

github.com

終わりに

如何でしたでしょうか?1行のコード修正と言っても、ドキュメントや関連するソースを追いかける必要が有り、サクっと終わるわけではないですが、解決方法が分かると成長した感を実感できますね。

ほかにもドキュメントのtypoを直す、チュートリアルが最新バージョンと合っていない所を直す、サンプルコードを直す・追加する等、貢献できるところは色々と有るので、ぜひチャレンジして見て下さい。

その途中で調べることや試行錯誤することがたくさん出てきて、周辺知識がどんどん身についていくと、単純な修正だけでも得られることがたくさん有ることを実感できるのではないでしょうか。