1️⃣

AtCoder Beginners Selectionの1問目をnim言語(1.6.14)で解く

2023/11/15に公開2

はじめに

nim言語は非常に表現力豊かな言語です。
これが意味するところのひとつは、同じ動作をするコードが何通りもあり、何通りでもかけてしまう、ということです。
ですから、書き手の好みが顕著に現れます。私のコードの書き方もかなり趣味趣向が表れているかと思いますので、ここに書いてあることのみが正解なわけではないということを念頭に置いてお読みください。

つぎに

この章は無視しても問題ないです。ソースコードを呼んで疑問が出たら読み返してください。

では、私の趣味趣向について少し書いておきます。
わたしはもともとpythonを使っており、動作の遅さについて少し不満がありました。
それを解決するためにnim言語を使い始めたため、python的な部分が見えるかもしれません。
また、それとは別のこだわりのために少々ややこしいことをしているかもしれません。

具体的には以下の通りです。

  • 前提となるコード
    • 必要だと思われるものをすべてimportしておく
      • nimはpythonに比べて何もimportしない際の機能が最低限に抑えられており、競プロのようなシチュエーションではあらかじめimportしておくほうが効率的だと思ってそうしています。
    • 競プロ的に無視したいhintやwarningを切る
      • アプリケーションの開発では必要なwarningやhint(弱いwarning)を無視するようにpragma({. .} で囲って表記し、コンパイラに情報を伝える機能)を利用して無効化しています。
    • toTupleマクロ
      • 可変長配列(pythonでいうlist的なもの)をタプルという固定長のものに変換するマクロ(コンパイル時にソースコードを生成する)を利用しています。
      • 整数nを受け取り、前からn個をタプルとして固めて返します。
      • これは、標準入力から受け取った値を代入する際にタプル的代入を利用するために使用しています。

前提コード

import std/[sequtils, strutils, strformat, strscans, algorithm, math, sugar, hashes, tables, complex, random, deques, heapqueue, sets, macros]
{. warning[UnusedImport]: off, hint[XDeclaredButNotUsed]: off, hint[Name]: off .}

macro toTuple(lArg: openArray, n: static[int]): untyped =
  let l = genSym()
  var t = newNimNode(nnkTupleConstr)
  for i in 0..<n:
    t.add quote do:
      `l`[`i`]
  quote do:
    (let `l` = `lArg`; `t`)

# --------------------------------

基礎知識

単語の説明を並べています。分からない単語などがあれば質問してください。追加します。

  • 標準入力: 競プロにおいて入力が与えられる場所です
  • 標準出力: 競プロにおいて出力を求められる場所です
  • string:
    • 文字列(文字のが並んだもの)
  • シーケンス, seq
    • 可変長配列(実行中に長さの変わる配列)
    • 要素として許されるのは単一の型のみ(intだけ, stringだけなど。混ぜれない)
    • (pythonでいうlist的なもの)
  • tuple
    • 固定長
    • 複数の型を含んでよい(intとstringを含む長さ2のタプル、が作れる)
  • let文
    • 代入を行う
    • 一度letを使って代入すると書き換えできない

では、これから問題を解いていきます。

PracticeA - Welcome to AtCoder

import std/[sequtils, strutils, strformat, strscans, algorithm, math, sugar, hashes, tables, complex, random, deques, heapqueue, sets, macros]
{. warning[UnusedImport]: off, hint[XDeclaredButNotUsed]: off, hint[Name]: off .}

macro toTuple(lArg: openArray, n: static[int]): untyped =
  let l = genSym()
  var t = newNimNode(nnkTupleConstr)
  for i in 0..<n:
    t.add quote do:
      `l`[`i`]
  quote do:
    (let `l` = `lArg`; `t`)

# --------------------------------

let 
  A = stdin.readLine.parseInt()
  (B, C) = stdin.readLine.split.map(parseInt).toTuple(2)
  S = stdin.readLine

echo &"{A + B + C} {S}"

標準入力から整数と文字列を受け取り演算して返す、という問題です。

複数行のlet文をインデントで一塊にしています。

Aは、stdin.readLineによって標準入力から一行読んだ後、それをparseIntでintに変換しています。(標準入力 -> "1" -> 1 に変換)

B, Cは、stdin.readLineで一行読んだ後、splitで空白によって分割、分割されたそれぞれに対してparseIntを行い(map)、それを固定長であるtupleに変換しています。(標準入力 -> "2 3" -> @[2, 3](シーケンス) -> (2, 3) に変換)
let (B, C) = (2, 3)とすると、Bに2が、Cに3が代入される、というタプル的代入を利用しています。

Sは、stdin.readLineによって標準入力から一行読み込んでそのまま文字列を代入します。

echo &"{A + B + C} {S}"は文字列をフォーマットした後にechoで標準出力に出力しています。
&""で文字列の内側に{}を使って式が書けるようになるのでそれを利用してA + B + CSを埋め込みます。
(echoはpythonでいうprintです。)

メソッド的記法

stdin.readLineをいう表記が多用されていますが、これはreadLineの第1引数としてstdinを渡すという意味です。(???)
すなわち、readLine(stdin)stdin.readLineと同じ意味であるということです。

私のコードではメソッド的記法を利用しています。ただ、let文の中身を書き換えることでこのように書くこともできます。(動作は同じです)

let 
  A = parseInt(readLine(stdin))
  (B, C) = toTuple(map(split(readLine(stdin)), parseInt), 2)
  S = readLine(stdin)

nimではいわゆるメソッドといわゆる関数の区別がなく、記法の違いとして扱います。
これにより、使い方によっては可読性や書きやすさが向上します。

コマンド的記法

echo "Hello"echo("Hello")は同じ意味です。なのでどちらで書いても問題なく動作します。

echo(&"{A + B + C} {S}")

おわりに

nim言語が非常に表現力豊かであることの一面を表している内容だったと思います。
このほかにも非常に強力な記法があり、競技プログラミングとの相性の良さもかなりあると思います。
(pythonと見た目が似ていることから移行先としてもいいかなと思います。)

書き始めたときは10問分書こうと思っていたのですが、1問目で長くなりすぎたので終わります。質問や誤った記述に関する指摘などがあればお願いします。

Discussion

NeoNeo

zennでのnim関連の記事のタグは基本的にnimで統一されているので、追加しておくといいかもです🙇‍♂️