ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

バグを少しでも(楽に)なくしたいエンジニアの言語選択についてのお気持ち

この記事はユニファAdvent Calendar 2021の13日目の記事です。

adventar.org

こんにちは。プロダクトエンジニアリング部の安田です。

この記事でかくこと
  • バグって何で起きるのか自分の経験を図にしてみたこと
  • TypeScriptの言語機能を使ってバグを防ごうとする
  • Elmの言語機能を使ってバグを防ごうとする
時間がない人向けのこの記事で伝えたいことまとめ
  • バグを完全になくすのはとても難しい
  • TypeScript素敵です。
  • Elmも素敵です。
  • 安全なサービスを世に送りつづけるため、皆で工夫を続けよう。
  • ユニファは仲間を募集中です!

バグってどこから生まれるの?

さて今年も残すところあと少しですね。この1年は皆さんどんな一年だったでしょうか? どのような関わり方かは問わず何らかのサービスを世に発信している方は、利用者の課題を解決することへの喜びを抱えながら、要望を満たすためのリリースを繰り返す中で、少なからず様々なバグ(不具合)とも向き合ってきたのではないかと思います。 そもそもバグは一体どこから生まれてくるのでしょうか? バグと一言にいったとしても、受け取る人によって浮かぶことは様々だと思います。そこでまずはこの記事で扱う「バグ」という言葉の定義を一度しておこうと思います。一般的に辞書で定義されているものとは多少異なる可能性があります。予めご了承ください。

「バグ」のこの記事での定義

何らかのシステムを構成する際に、そのシステムで期待される処理に対して誤った処理をしたことから、エンドユーザ、システム管理者に大小問わず影響を及ぼす事象のこと。

とある開発者視点では

この記事をかいてる人間は、普段はエンジニアとして少しコードを書いてるので、その目線から世の中をみています。その上で、「バグはどこから?」を書き出してみました。

f:id:unifa_tech:20211210113716p:plain

沢山ありますね。粒度やレイヤーが同じではないものもあります。そこの矛盾は許容させてください。あくまで1個人の経験を発散させたものなのです。

f:id:unifa_tech:20211210113748p:plain

次に、少しグループに分けてみました。

  • 仕様が不明確、もしくは理解が足りてないこと
  • 扱うプログラムを理解することが困難であること
  • 変更に伴う何らかの破壊

あくまで個人の意見ですが、このあたりがバグをうむ主な原因ではないかと考えます。 ただ仕様の正しい理解については開発者だけではなく、チーム全体で取り組むべき部分なので開発者だけで有効な解決策を見つけることが難しいです。それ以外の2つについては開発者だけでも取り組むことができるため、この記事ではこの部分を深掘りします。

理解しやすく、変更に強い。そんな開発が可能なのでしょうか?

バグを減らすための無数にあるアプローチの中で、今回は「言語選択」に焦点をあてたいと思います。 現在様々なプログラミング言語が世の中には存在しており、開発者にとってバグを減らしやすい言語選択というのは可能だと思います。 もちろん、言語を選択する時には、他にも多くのことを考えなければいけませんし、選択したからといってバグを0にすることは非常に難しいことで、出来る開発者もいると思いますが、少なくともこの記事を書いてる人間は今回紹介する言語で書いたとしてもバグを出してしまうと思います。わからない事や出来ない事も沢山あります。でもバグは減らしたいと思っていて、出来る工夫を少しづつでも取り入れています。

この記事では、TypeScriptと、Elm、2つの言語を取り上げて、それぞれの言語のバグを防ぐための機能について紹介していきたいと思います。「この言語しか駄目」と視野を狭めることは望ましくないので、一つの意見、一つの選択肢として捉えていただければと思います。

この記事を届けたい読者 🙇🏻‍♂️

  • TypeScript ? Elm ? どんななの?知りたい!という人
  • 静的型付け言語を全く触ったことのない人
  • 開発者がサービスを作るために黒い画面に何をぽちぽち入力しているのか知りたい人

期待に応えることが出来ないかもしれない読者 🙇🏻‍♂️

  • TypeScriptやElmを使ってきちんと一通り書けるようになりたい人
  • 関数型言語についての説明が欲しい人
  • バグを完全になくす方法が知りたいという人

さて、どういった工夫が出来るでしょうか。それぞれの言語で、どうやってこれらの事象を防ごうと出来るのかどうかあげてみます。

  • 扱うプログラムを理解することが困難であること
  • 変更に伴う何らかの破壊

どのような機能が私たちを助けるるのか

TypeScript

※記載しているコードはVersion 4.5.2にて動作確認をしています。

まずはTypeScriptについてです。(公式サイト

TypeScriptは絶賛勉強中です。オライリーの本

www.oreilly.co.jp

が自分にはわかりやすかったです。*1

「〜がない」という表現の豊さ。

Typescriptを勉強する時に、まずびっくりしたのは「〜がない」の表現が複数ある事です。

undefined , void , never , null , の4つです。 undefined は他の言語でも目にする機会が比較的多いかもしれません。

以下の2つは、「存在がない」ことを表す値であり、

  • undefined :まだ定義されていない事
  • null:値が欠如している事

残る2つは、「実行した結果の存在がない」ことを表す関数の型です。

  • never:実行しても決して戻らない関数の戻り値の型
  • void:何も返さない関数の戻り値の型

プログラムを書いていると、たびたび、「〜がない」というエラーに遭遇します。そしてそれは開発中だけではなく、リリースした後にもバグとして露見することがあります。 これらの「〜がない」をコード上に表現しやすくなることで、それらのエラーを防ぐことができます。

TypeScriptの型システム

TypeScriptの型推論についても取り上げるのがいいと思います。 型推論は自動で私たちが書いたコードを型づけしてくれる機能ですが、影のように働きつつ私たちの書くコードのバグを暴くことがあります。何も書いてないのにも関わらず、私たちに教えてくれるのです。

function double(n) {
  return n * 2
}

非常に小さな関数です。パラメータを一つ受け取り、2をかけて返します。 どの言語でもこういった受け取ったパラメータに何かをして返すという入力値に依存したコードというのは存在すると思います。 ただこのコードを書くとTypeScriptはコンパイルエラーを出します。 TypeScriptのおかげで、私たちはコードを書いた時にこのエラーに気づくことができ、利用者にサービスを公開する前に危ない箇所を見つけられました。テストを1行も書いてません。これは素晴らしいことです!

どこが駄目なのでしょうか?

7006: Parameter 'n' implicitly has an 'any' type.(パラメータnは、暗黙的にany型をもってます。)

any 型は全ての型の属性をもつ型です。つまり、何でも来ちゃいますよ、という意味です。 例えば、先ほどの「値がない」 null が渡されてきたとしても、 n * 2 が実行されようとしてしまいます。 「そうなるとエラーになるぞ」ということでTypeScriptは教えてくれます。 受け取った引数に依存する処理がある時に教えてくれます。なので、型アノテーションをつけてあげます。

function double(n: number) {
  return n * 2
}

こうすることで、TypeScriptもOKを出し、コンパイルエラーが解消されます。 折角作ったので、関数をよんでみましょう。

double('hello!')

途端にTypeScriptはまたエラーを吐き出します。(読んでいる読者は溜息を吐き出しているかもしれません。)

Argument of type 'string' is not assignable to parameter of type 'number'.
(引数の文字列型は、数値型のパラメータには入れられませんよ)

・・・そうです。うっかり文字列型を渡してしまいました。 非常に単純な例を書いてるので「いやいやそんなこと書くわけない。さすがに。」と思う方もいるかもしれません。確かにそうです。 でも、もしこの引数が動的に生成される値だったら?

幾度となくバグを生み出してきた原因の一つではないでしょうか。 この引数がどのように作られたのか、何度変更されたのか、に関わらず、TypeScriptはきちんと気づいて怒ってくれます。

let num = 3
num = ‘three’  // Type 'string' is not assignable to type 'number'. (文字列型は、数値型に入れられません)
num = null // Type 'null' is not assignable to type 'number'.  (null型は、数値型に入れられません)
double(num)

もう少し複雑な関数の例をみてみます。

function isString(s: unknown) {
  return typeof s === ‘string’
}

何か( unknown 型はまだ何が解らない、という型です)を受け取って、その値が数値型であるかどうかを返す関数です。 これを以下のように使用してみます。

function parseInput(input: string | number) {
  let formattedInput: string
  if (isString(input)) { // * 文字だったら大文字にする
    formattedInput = input.toUpperCase()
  }
}

if文を書いて、何かの処理を実行するかどうかを分岐させるのはよくあることだと思います。こと型の絞込みに関してはTypeScriptではちょっとした工夫が必要なので紹介します。 一見すると問題なさそうなコードです。ですが、TypeScriptはエラーを出します。

Property 'toUpperCase' does not exis...UpperCase' does not exist on type 'number'.
('toUpperCase'は、数字には存在しません。)

おかしいですね。* では文字列型であることをif文で絞り込んでいると思います、、

関数 isStringboolean を返すことは型推論で既にTypeScriptはわかっていますが、どの時に true を返すのかわかりません。明示的に書いてあげる必要があります。 型の絞り込み( typeof )に関しては、スコープないだけ有効であり、呼び出し元、つまり*の部分では引き継がれなかった、ということになります。

function isString(s: unknown): s is string {
  return typeof s === ‘string’
}

ユーザー定義型ガードというものを使うこと( s is string )で、引数 sstring の場合 true を返すという絞り込みを引き継げるようになります。 型注釈*2や、型推論*3は、私たちがどんなコードを書いているのかを見直すきっかけを与え、バグの少ない読みやすいコードを書けるように助けてくれます。

型注釈については個人的には以下の理由からなるべく書いた方がいいと思っています。

  • 自分の理解が間違っている場合は、エラーによってすぐに気づくことができる。
  • コンパイラを通すために型注釈を書く、という考え方ではなく、型注釈を残すこと自体がドキュメントとしての価値が高いため、開発者のために書く。

もちろん、型注釈をつけるつけないの方向づけはチーム内で対話して決めていくべき事柄です。 書かれていることに意味を探す人もいれば、書かれてないことに意味を探す人もいます。 そういった視点では書かなくてもいいものを書くことに対する冗長さをノイズと感じる可能性もあります。

TypeScriptの魅力や色々な機能は他にもありますが、Elmの枠があるので、このくらいにします。

Elm

※記載しているコードは0.19.1にて動作確認をしています。

次はElmについてです。(公式サイト

TypeScriptに比べ、Elmという言語はマイナーかもしれません。 ElmはHaskell(公式サイト) の影響を受けた言語です。Haskellを勉強すると、これはどこで使う機会のある処理なんだろうとwebアプリケーションを作る上では疑問に思う部分がある(これは何もHaskellに限った話ではないと思います。)のですが、Elmは関数型言語としての特徴は残しつつ、わりとライトにかける言語ではないかと思います。

Elmにはフレームワークとしての側面がありますが、今回取り上げるのはフレームワークとしての特徴ではなく、言語機能としての特徴のみにします。ぜひ、フレームワークとしてのElmも機会があれば是非調べて触れてみてください。

nullに対するElmの表現方法

ElmもTypeScriptと同じくコンパイル時に、危険な箇所をエラーとして私たちに教えてくれます。 触れるべき言語機能的特徴を、記載します。Elmにはnull参照がありません。 何かの値がnullだった時に実行しようとする処理は他の言語はエラーを出すことが多いですが、Elmにはそれがありません。 どういったアプローチをElmがとっているかというと、「nullかもしれない」を明示的にコードに残すことで表現します。Maybe型がそのための手段です。

Elmでは以下の原因についてどう対応するのかという点を取り上げます。

  • 変更に伴う何らかの破壊

基本的にElmの変数の値は、変更ができません。同一スコープ内での再代入*4は禁止です。immutableを掲げている言語は他にもあります。(既に登場したTypeScriptでも const を使って宣言すれば、オブジェクト型以外の変数は再代入を禁止することができます。)

a = 1
a = 2 // この時点でコンパイルエラーは起きる。
入れたい場合は当たり前のことになってしまうかもしれませんが、
a = 1
a2 = 2
のように別の名前にしましょう。
Elmの関数について

加えてElmの関数についても記載します。 例えばTypeScriptの項目でも書いた簡単な関数を作ります。

function double(n: number) {
  return n * 2
}

このような形でした。 Elmではこう書きます。

double : number -> number
double n = n * 2

1行目が型注釈で、 2行目が関数定義です。

書き始めた時の想定よりもだいぶ長くなってしまいました。明らかにペース配分を誤り、Elmの記述に関して力尽きてしまいました。いつか機会があればまたどこかで、、 Elmについて少しでも興味を持ってちょっと勉強しようかなという方は「基礎からわかる Elm 著者:鳥居 陽介」がおすすめです。(その場合はkindleでマーカーを引くのが難しいので、紙の本を選択した方がいいかもしれません。kindleで持ってる人からの細やかなアドバイスです。) もちろん公式ドキュメントからはじめてみるのもいいです。 Elm-jpの皆さんが公式ドキュメントを日本語に翻訳してくださっています。いつでも皆さんの側にいます。

そろそろまとめようと思います。

まとめ

書き始めてから長い旅でした。お疲れ様でした。読んでくださってありがとうございます。 今回お伝えできた部分もできなかった部分もありますが、少なくとも、開発者の思い違いを指摘し、コードへの理解を深める機会をくれる そんな言語たちの存在を少しでもお伝えできていたら嬉しいです。

最近めっきり寒くなったので風邪に気をつけて開発楽しみましょう。

ユニファは仲間を絶賛採用中です〜!

unifa-e.com

*1:プログラミングTypeScript ――スケールするJavaScriptアプリケーション開発 Boris Cherny 著、今村 謙士 監訳、原 隆文 訳

*2:: unknown といったその値がどの型であるかをコード上に記載する手段です。

*3:既に述べていますが、開発者が型を指定しなくても値から自動で型を判別してくれる機能です

*4:関数型言語としては束縛(bind)されるという表現が適切ですが、この記事では代入という表現を使ってきたので代入という表現を使用させてください。