13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

弥生Advent Calendar 2022

Day 5

Nimに入門し宣言的UIライクでオニオンアーキテクチャ風なタイピング練習アプリ(CLI)を作ってみたかった

Last updated at Posted at 2022-12-04

こんにちは。普段は弥生系列のアルトア株式会社でRailsエンジニアをしています。今回Nim言語という存在を知ったので試しに小さなアプリ作ってみたら色々派生して勉強になったよという記事を書きました。

作ったもの

poketyping_demo.gif
英語版ポケモンの説明文を使ってタイピング練習ができるCLIアプリです。MacOSのみですが、バイナリで動くものも用意したのでGithubのリリースからダウンロードしていただければローカルのターミナル上で実行できると思います。(実行権限は自分で付与する必要があるかもしれません。)
それかリポジトリをCloneしてDockerで動かすか、Nimをインストールしていただければ動きます。

ちなみにお気に入りのポケモンはブラッキーでアッキの実持たせて物理にも特殊にも固い構成が好きです。

Nimってなに?

先人のありがたい記事がありますのでそちらをご参照ください。

なぜ作った

  • 「1年に一つは新しい言語に触れよ」という誰かのお言葉にならって新しい言語を勉強したかった
  • 普段Rails開発では触れないアーキテクチャの理解が足りなさすぎるので入門したかった
  • 前々からタイピングアプリ作ってみたかった

結果的にいろんなことが学べてすごく楽しかったです。

ポイント・機能

 ・PokéAPIからポケモンの情報をランダムに取得
 ・ポケモンの説明文をタイピングワードとして使用する
 ・デフォルトでは6匹分のタイピングをすると終了
 ・何匹倒すかはオプションで指定可能(6匹は結構長い)
 ・字幕としてローカル言語を見ながらタイピングできるので英語学習にも使える

※新作スカーレッド・バイオレットのポケモンは入っていないのでネタバレはありません。ご安心ください。

参考にしたCLIタイピングアプリ

実装の足がかりとしてこちらのリポジトリを大変参考にさせてもらいました。

アーキテクチャ

オニオンアーキテクチャ

  • presentation, application, domain, infrastructureに分離
  • 依存関係が1方向になるように気をつけました

宣言的UIライク

  • 再利用可能なコンポーネント単位をWidgetという形で表現
  • Widgetにデータを渡すことでページ全体を描画する
  • ステートが更新されたらWidgetを破棄し、新たにWidgetを生成して描画(Flutter的な挙動)

全てのViewに関するオブジェクトはWidgetを継承し、renderメソッドをオーバーライド実装するルールにします。

# ベースとなるオブジェクトとメソッドを定義
type Widget* = ref object of RootObj

method render*(self: Widget) {.base.} =
  discard

子ウィジェットは実際の画面描画を行う役割。

# シンプルな文字列を表示するウィジェット
# Widgetを継承する
type PureTextWidget* = ref object of Widget
  text: string

# コンストラクタ
func newPureTextWidget*(text: string): PureTextWidget =
  return PureTextWidget(text: text)

# renderメソッドのオーバーライド
# CLIなのでコンソールにプリントする
method render*(self: PureTextWidget) =
  stdout.write(self.text & "\n")

親ウィジェットは子を順番にrenderしていく役割。

# FlutterのColumn風なWidget
type ColumnWidget* = ref object of Widget
  children: seq[Widget]

# コンストラクタ
func newColumnWidget*(children: seq[Widget]): ColumnWidget =
  return ColumnWidget(children: children)

# renderメソッドで子ウィジェットのrenderメソッドを呼び出す。
method render*(self: ColumnWidget) =
  for child in self.children:
    child.render

最終的に描画される画面のコードはこちら。Columnウィジェットに部品ウィジェットが内包される形にしている。

proc buildScreen(self: TypingScreen): Widget =
  let cursorOffset = if self.gameState.noLocal: 0 else: 4
  return newColumnWidget( # Columnウィジェット内に表示したいウィジェットを順番に入れていく(この辺Flutterを意識)
            children = @[
              new Widget, # sequenceがWidget型であることを認識させるためのおまじない。
              newPureTextWidget(text = "Type first character to Start! [CTRL-C] or [ESC] to exit."),
              newFrameTopWidget(totalPokemon = self.gameState.totalPokemon, remainingPokemon = self.gameState.remainingsCount),
              newTextAreaWidget(text = self.gameState.currentPokemonName),
              newBorderWidget(),
              newTextAreaWidget(text = coloredText(self.gameState.currentText, self.gameState.judgeResults), row = 4),
              newBorderWidget(hidden = self.gameState.noLocal),
              newTextAreaWidget(text = self.gameState.currentLocalText, row = 3, hidden = self.gameState.noLocal),
              newBorderWidget(),
              newTextAreaWidget(text = self.gameState.wroteText.split('*')[^1]),
              newFrameBottomWidget(),
              newCursorWidget(posX = baseCursorPosX + self.gameState.wroteText.split('*')[^1].runeLen, posY = baseCursorPosY + cursorOffset )
            ]
          )

# renderが呼ばれると、現在のコンソール画面をリセットして、内包するWidgetを再描画する
method render*(self: TypingScreen) =
  self.screenReset()
  self.buildScreen.render

学んだこと

依存関係とか副作用を意識した

オニオンアーキテクチャを意識して個々の層の役割だったり依存の方向性を意識しながらコーディングしました。依存関係は1方向に制限することが出来たと思いますが、プレゼンテーション層にモデルそのものを渡すような構造になっていたりします。Adapterパターンなどを使用して、モデルそのものを渡さず、Viewが必要な情報だけ加工してあげるような作りにしても良いと思いました。(規模次第なのかな?)
また、Nimは副作用の有無によってメソッドの宣言方法を変えることが出来ます。副作用のあるメソッドはproc、ないメソッドはfuncを使って宣言します。できるだけ副作用のないfuncを使いたくなって、副作用をすごく意識するようになりました。

ゲームアプリのステート管理がわからない

今回作ったアプリは常に次のキー入力を待ち、キー入力ごとに状態を変化させるアプリですが、ステート管理に悩みました。最終的にはステートをUseCaseに書いていて、ずっとUseCaseインスタンスが常駐している状態になっています。多分駄目な構成です。ステートは別に持って、入力と外のステートをUseCaseに渡して上げる形にして、UseCase自体はステートレスに作ったほうが良い気がします。(もう一回作り直すならもう少し考えられそう。。時間切れ。。)この辺りはReactとかFlutterのステート管理を勉強しないとなと思いました。

Nimはシンプルで書きやすく楽しい

Nimでお気に入りのものがresult変数というものです。どういうものかというと、関数内のresultという変数は宣言不要で返り値の型が指定されており、かつ、returnを書かなくてもresultの最後の状態がreturnされるというものです。全ての関数でこのように統一することによってどういう計算の元で最終結果が作られるのか追いやすくなり、可読性が上がりそうだと思いました。

proc complex_calculation(a: int, b: int): int =
  #
  # 複雑な計算処理
  #
  result = a + b # resultの中身がreturnされる

一方で、

Rustや他の有名言語ほど流行っていないので、欲しい物が揃っていないことがあります。実際に、コンソールへの画面描画のためにマルチバイト文字の文字幅を計算するライブラリに求めていたものがなかったので自作してライブラリとして公開したりしました。(その結果Unicodeの公式サイトを行ったり来たりして少し文字幅問題について理解することが出来ました。)

終わりに

興味があれば是非Nim触ってみてください!

最後まで読んでいただきありがとうございました。

13
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?