87
55

More than 5 years have passed since last update.

型を使って意味を表現する

Last updated at Posted at 2017-12-11

型を使って意味を表現する

この記事はC++アドベントカレンダー2017の11日目の記事です。

C++は静的型付けです。皆さん型使ってますか? 型をつけましょう。

どうせ型をつけるなら、上手くやっていきましょう。型を効果的に使えば、関数なんかも宣言を見ただけで何をするかだいたいわかるようになります。

型はコンパイラがチェックしてくれるという利点があります。これは利点です。コンパイラは謎のメッセージを出して怒り狂う予測できない荒ぶる神ではなく、こちらのミスを指摘してくれる優しいチューターです。こちらがしたいことを型のレベルで表現すれば、我々がミスをした時にコンパイラは助けになってくれます。

今回の記事の内容は、一言で言うと「起き得ない状態を起き得なくする」ということです。言葉を足すと、「意味的に起き得ない状態は取れないような型を使う」ということです。

例が必要でしょう。上記の気持ちを中心に据えて、いくつかの機能の使い方を紹介します。

optional

optionalは、「期待する型の有効な値を持つか、または値が存在しない」ということを意味する型です。

それはどういうことでしょう? 「起き得ない状態を起き得なくする」という観点から、これはどういう点で使えるのでしょう。

例:無いかも知れないメンバ変数

例えば何かのプロジェクトで、ユーザーはあるオブジェクトに正確な名前の他にエイリアスを付けられるとしましょう。どうなるでしょうか。

struct ID
{
    std::string name;
    std::string alias;
    bool has_alias;
};

このクラスはどのような状態をとり得るでしょうか。

  1. has_alias() == true && !alias.empty()
  2. has_alias() == true && alias.empty()
  3. has_alias() == false && !alias.empty()
  4. has_alias() == false && alias.empty()

ここで、状態1は正しいです。has_aliasが真で、かつaliasが値を持っている。あり得る状態です。また、状態4も正しいです。has_aliasは偽であり、またalias自体も空です。他はどうでしょう。状態2で、has_aliasが真なのにaliasが空なら、どうすればいいでしょうか。空文字列がaliasなのでしょうか。それを許可しているなら構いませんが、少し奇妙な状態です。また、状態3も怪しいです。has_aliasは偽なのに、aliasには何かが入っています。この場合どちらを優先させればいいのでしょうか。

このような実装をした場合、aliasの値とhas_aliasの値に齟齬があってはいけません。これらを操作するときは、全ての段階でこの2つを同時に扱い、常に値を同期させておかねばなりません。これは面倒です。

optionalを使うとどうなるでしょうか。

struct ID
{
    std::string name;
    std::optional<std::string> alias;
};

同じことを意味できていることは明らかです。aliasはoptionalです。あってもなくても構いません。そして値がある場合は、std::stringとしてとり得る全ての値を取れます。

先に述べた問題が解消されていることにお気づきでしょうか。std::optionalなら、値が存在してある具体的な値を持つか、値がないかのどちらかです。値が存在しないのに具体的な値が入っていることはありません。alias.has_value()かつalias.value().empty()はあり得ますが、この場合aliasがないのだろうか、と悩むことはありません。aliasの値は あります。 なので平然と空文字列を使うか、エイリアスが空だというエラーを出していいわけです――少なくとも意味的には。

恐らく空文字エイリアスは、ユーザーの誤入力か、aliasを削除する際にstd::nulloptの代入もしくはstd::optional::resetの呼び出しでなくstd::string::clearを使ってしまったことによるものでしょう。そうとわかれば修正も簡単です。

ここで少しstd::optionalに付随する文法を紹介しておきましょう。

{
    std::optional<std::string> optstr;
    std::cout << std::boolalpha << optstr.has_value() << std::endl; // "false"
}
{
    std::optional<std::string> optstr = "Hello, optional!"s;
    std::cout << std::boolalpha << optstr.has_value() << std::endl; // "true"

    if(optstr.has_value())
    {
        std::cout << optstr.value() << std::endl; // Hello, optional!
    }
    optstr = std::nullopt; // 破棄
    std::cout << std::boolalpha << optstr.has_value() << std::endl; // "false"
}

値があるかどうか確認するためにはstd::optional::has_value()を、値にアクセスするためにはstd::optional::value()を使います。また、既に値を持っているstd::optionalは、std::nulloptを代入することによって無の状態に戻せます。

値を取り出すとき、もし無だったら代わりの値を使いたいという場合はあるでしょう。デフォルトの値が決まっているときはその方が便利です。そのときは、std::optional::value_orを使います。

std::optional<int> answer = std::nullopt;
std::cout << answer.value_or(42) << std::endl; // 42

ちなみに値を持っていないstd::optionalの値を無理やり取り出そうとすると、std::bad_optional_access例外が投げられます。

例:してもしなくても良い処理

他に、外部の状況によってクラスのメンバ内でするべきかどうか変わる処理、というのもあり得ます。特にその処理が外部からパラメータを受け取る場合を考えます。

例えば分子ビューワを作っているとしましょう。原子間の結合を棒で描きたいかどうか、その太さも外部から設定できるようにしたいとします。

class Visualizer
{
    bool         draw_bond_;
    double       bond_width_;
    Molecule     molecule_;
  public:
    void visualize();
    void render_bond();
};

void Visualizer::visualize()
{
    // do something...

    if(this->draw_bond_)
    {
        this->render_bond();
    }
    return;
}

先ほどの例に比べると、デフォルトの値に苦労はしないかもしれません(もう少しよい例を思いつけばよかったのですが)。draw_bond_ == falseの時、bond_widthの値は恐らく0でしょう。それでも、ならdraw_bond_はなくて良いのではとか、引き算したら数値誤差でbond_widthが非常に小さいけれど0でない値になった場合はとか、考えないといけないエッジケースは少し残ります。

この場合も、std::optionalが使えます。外からパラメータ設定済みのオブジェクトを代入できるので、

class BondRenderer
{
    double width;
  public:
    void render(Molecule const&);
}

class Visualizer
{
    std::optional<BondRenderer> renderer_;
    Molecule molecule_;
  public:
    void visualize();
};

void Visualizer::visualize()
{
    // do stuff...

    if(this->renderer)
    {
        this->renderer_->render(this->molecule_);
    }
    return;
}

こうすれば、関数を呼ばなくて良い時にそのデフォルトパラメータをどうするか考慮しなくて済みますし、フラグとパラメータの値を同期させておく手間もありません。

外部から来るかもしれないパラメータをデフォルト構築するというのは少し面倒です。デフォルトパラメータの値によってはわかりにくいバグが仕込まれることでしょう。そのパラメータをoptionalにしておくというのも悪くはないです。
が、今回の場合、存在するかしないかわからないのは「関数のパラメータ」というより「実行すべき関数そのもの」なので、関数オブジェクト(今回は呼び出しがoperator()ではないですが……)を作ってそれをoptionalにしてした方が素直に思えます。

例:存在しない可能性のある値の取得

存在しない可能性のある値、と言われて何か思いつくでしょうか。例えば、std::vector::atstd::map::atが返すものがそうです。std::vector::atはインデックスがsizeを超えていればおかしな領域を見に行きますし、std::map::atもキーがなければ返すものがありません。これらは現在std::out_of_rangeを投げることでエラーを通知しますが、別の実装として以下のようなこともできたはずです。

std::optional<value_type const&> vector<T, Alloc>::at(size_type i) const noexcept;
std::optional<mapped_type const&> map<K, V, Cmp, Alloc>::at(key_type i) const noexcept;

このように実装していた場合、対応する値がなかった時、エラーが例外越しに通知されるのではなく、「無」が返ってきます。

const auto val = vec.at(N);
if(val.has_value())
{
    // do stuff
    std::cout << "vector has the N-th value!: " << val.value() << std::endl;
}
else
{
    // fallback 
    std::cout << "vector does not have the N-th value..." << std::endl;
}

受け取った側は、このようにして値があるかどうかを確認することができます。というより、必要な値を得るためには、一度値があるかどうか確認する処理を書く必要があります。

ところで、古来からこのような用途にポインタが使われてきました。失敗するとnullptrが返ってくる、ということです。面倒なことを全て脇に置けば、失敗したらnullptrで、成功したら値を指すポインタが渡されるというのはまさしくoptionalと言えるでしょう。そのせいか、std::optionalではポインタライクな文法を実現するための演算子が定義されています。

const auto val = optvec.at(N);
if(val)
{
    std::cout << "vector has the N-th value!: " << *val
              << " and its first value is " << val->first << std::endl;
}

まさにポインタですね。この文法に馴染んでいた人は、このコードを理解するのに苦労しないでしょう。has_valueを明示的に書くこともできるので、ポインタでのやり取りに慣れていない人にはそちらの方がわかりやすいとは思いますが。

ではポインタを返せばよいではないか、と思う人のために、ポインタに比べての利点を挙げてみます。インターフェースがより豊富な点は重要です。わかりやすい名前の関数を使うことができ、コードが誰にとっても読みやすくなります。また無理にアクセスした時には未定義動作ではなく例外が飛び、デバッグが容易になります。
コンテナアクセス以外の返す値が無いかも知れない処理で使う場合は、これらの他にもメリットを挙げられます。optionalは動的確保を行わず、内部に確保した領域を使うので(N4659 23.6.3)、何度もnewしてオブジェクトがメモリ上の至るところに分散してキャッシュ効率が落ちたり、アクセスするときにポインタを経由する手間が増えたりしません。deleteの必要もなく、リソース管理が楽です。
これらがポインタよりもstd::optionalをおすすめする理由です。

variant

variantは、あり得る型のどれか一つだけを取る型です。ある時点で保持している値は最初に列挙しておいた型のどれか一つに対応する値であり、それ以外の型を持ったり同時に複数の型の値を持ったりはしません。

それはどういうことでしょう? 「起き得ない状態を起き得なくする」という観点から、これはどういう状況で使えるのでしょう。

例:ゲームAI

あなたがゲームを作っているとしましょう。そして敵キャラクターはある程度賢く、無警戒で何もしていない状態、規定のルートを巡回している通常の状態、異変を感知して調べようとしている状態、主人公を発見して追いかけてくる状態の4状態を取るとします。

std::variantを使わない実装はどうなるでしょうか。

class Enemy
{
  public:
    enum class State : std::uint8_t
    {
        idling,
        normal,
        watching,
        engaged,
    };
    State    state;
    Position position;
    Position strange;
    Identifier target;
};

変数名は5秒で決めたのであまり深く考えないで下さい。

stateenumで敵の状態を、positionは敵の現在位置を、strangeは異変を見つけた(主人公が敵からは見えないところで発砲したなど)大まかな位置、targetはプレイヤーなどを追いかけるための識別子とします。

このコードだと、例えばstrangewatching状態でなければ必要がない値です。さらに、targetも戦闘中(engaged)でないと意味を持ちません。とはいえこの実装では、状態に依らずこの値を持つことになります。すると、戦闘中でない敵のtargetにアクセス「できる」ことになります。アクセスしないことになってはいても、不注意でしてしまったりできるわけなので、少し不安が残ります。

加えて、このクラスは本来必要ない量のメモリを要求します。同時に2状態を取ることはないので、strangetargetを同時に保持できる量のメモリは必要ないはずですが、この実装ではその両方を格納できる量のメモリを要求します。

ミスの可能性は人間の根性・努力ではなく設計で回避しましょう。std::variantは両方の問題を解決してくれます。

class Enemy
{
  public:
    struct Idling{};
    struct Normal{};
    struct Watch{Position strange;};
    struct Engage{Identifier target;};

    Position position;
    std::variant<Idling, Normal, Watch, Engage> status;
};

std::variantは同時に2つの状態になりはしないので、statusに格納されている値は常に上記4状態のどれか一つだけになります。これで、誤ってIdling中にtargetにアクセスしてしまう、というのは(悪意を持って非常に面倒で危険なことをしない限り)起き得なくなりました。

positionは常にありますが、これはIdlingでもこのキャラクターがフィールドに登場していると想定しているからです。つまり、どの状態でも必ず持つ値だと考えたからです。もしIdling状態が意味するところが「敵は生成されているもののフィールドには存在しない」ということなら、positionNormal, Watch, Engageの中に入れてしまっていいでしょう。

また、std::variantは格納すべき型のうち最大の型のサイズ分だけのメモリ(と、何を格納しているかを示すstd::size_t)しか要求しません。なので、メモリ量の問題も解決しています。


ではアクセスするにはどうしたらいいでしょうか。元のクラス、std::variantを使わない場合は、これはenumでスイッチするでしょう。

class Enemy
{
  public:
    enum class State : std::uint8_t
    {
        idling,
        normal,
        watching,
        engaged,
    };
    State    state;
    Position position;
    Position strange;
    Identifier target;
};

void process(Enemy const& em)
{
    switch(em.state)
    {
        case Enemy::State::idling:
            return process_idling_enemy(em);
        case Enemy::State::normal:
            return process_normal_enemy(em);
        case Enemy::State::watching:
            return process_watching_enemy(em);
        case Enemy::State::engaged:
            return process_engaged_enemy(em);
        default:
            throw std::logic_error("invalid enum state");
    }
}

これと同様のことは、std::variantでもできなくはないです。std::variant::index()は、どの型を保持しているかを意味するstd::size_tを返すので、以下のようにできます。このstd::size_tstd::variantに登録した順になります。

class Enemy
{
  public:
    struct Idling{};
    struct Normal{};
    struct Watch{Position strange;};
    struct Engage{Identifier target;};

    Position position;
    std::variant<Idling, Normal, Watch, Engage> status;
};

void process(Enemy const& em)
{
    switch(em.index())
    {
        case 0:
            return process_idling_enemy(em);
        case 1:
            return process_normal_enemy(em);
        case 2:
            return process_watching_enemy(em);
        case 3:
            return process_engaged_enemy(em);
        default:
            throw std::logic_error("invalid state");
    }
}

ただ、これではあまりわかりやすくはありません。というのも、std::variant::indexが返すのはenumと違ってstd::size_tなので、コードにマジックナンバーが入ってしまうからです。実際、上のswitch-casestd::variantに何か新しい値を入れた時はそれぞれの関数の内部を全て書き換えなければなりません。例えば、即座に出てくる方法としては、std::variantを持つクラス内でenumを作って名前を付けることでしょう。

class Enemy
{
  public:
    enum class Status : std::size_t
    {
        idling   = 0,
        normal   = 1,
        watching = 2,
        engaged  = 3,
    };

    struct Idling{};
    struct Normal{};
    struct Watch{Position strange;};
    struct Engage{Identifier target;};

    Status which() const noexcept {return static_cast<Status>(status.index());}

    Position position;
    std::variant<Idling, Normal, Watch, Engage> status;
};

これでもできますが、若干面倒です。第一、このenumの対応する値は状態が増えた時は変更しないといけません。その程度、自動でやってほしいではないですか。

std::variantにはより強力なアクセサが用意されています。大抵の場合、std::variantに入れるようなクラスは意味的な繋がりがあり、同じ関数を適用することは多いでしょう。つまり、オーバーロードによって一纏めにしておけるわけです。
それらのオーバーロードをまとめたvisitorというものを定義して適用すると、内部的なオーバーロード解決によって適切なものが選ばれます。つまり、

struct processer
{
    Position pos_;
    void operator()(Idling const&);
    void operator()(Normal const&);
    void operator()(Watch  const&);
    void operator()(Engage const&);
};

のようなものを用意しておけば、

void process(Enemy const& em)
{
    return std::visit(processor{em.position}, em.status);
}

のようにして一気に適用できます。
ちなみに、ここでは明示的に全状態のオーバーロードを書きましたが、ジェネリックに書くことも出来ます。

例:パーサ

他にも、値として整数、浮動小数点数、文字列、配列を持つような言語のパーサを書くとき、そのvalueクラスはまさしくstd::variantになります。値は言語が許可している型のいずれかになり、またいずれか一つだけだからです。

struct value;
using integer  = std::int64_t;
using floating = double;
using string   = std::string;
using array    = std::vector<value>;

struct value
{
    std::variant<integer, floating, string, array> holder_;
};

C++17からstd::vectorの宣言に、条件付きですが不完全型を使えるようになりました(N4659 26.3.11.2)。なので上のようなことができます。たぶん。不完全型でもポインタのサイズは決まるので、ポインタしかメンバに持たないようにすれば(std::array以外の)コンテナは不完全型を格納できます(ただし、メンバ呼び出しは型が決まってからである必要があります)。
それ以前だと、不完全型を持つ部分はポインタを持つようにしてそこだけ動的に管理するか、不完全型を許容して標準と同等のインターフェースを提供するboost::container::vectorを使うとよいでしょう。

空にならない保証

ところで、std::variantは「あり得る型のどれか一つだけ」を取る型です。これはつまり、「どれでもない状態」にはならない、要するに「空にならない」ということです。std::variantが空にならないことは保証されています。

それでもたまに、std::variantを空にしたいことはあるでしょう。空か、ある型か、別の型、という風に。また、これは少し実際的な話ですが、デフォルトコンストラクタを持たない型しか使わない時はstd::variantを具体的な値なしで初期化することができなくなります。この問題は空の状態を許容することで解決しますし、そういう状態が回避不可能というのは、そもそもその型は空の状態を必要としているということでしょう。

というわけで、空の状態がほしいという要求と、「あり得る型のどれか一つだけ」しか取らないという安全性を同時に満たすために、std::monostateが追加されました。これは、その名の通り一つの状態しか取らない型です。値が一つしかない以上、その値以外の奇妙なものには成り得ません。これは実質的に空であるかのように扱えます。

もちろん、std::optional<std::variant<A, B, C>>でも良かったはずなのですが、std::visitが空の状態とA, B, Cの全ての場合に対して同じやり方ではvisitできないため、std::monostateが導入された、と私は理解しています。あと、std::optional<std::variant<A, B, C>>だと本当にほんの少しだけサイズが大きくなる気がしますね。std::optionalは保持すべき値+保持フラグ(bool)を持つのでstd::variantより1バイトだけ大きくなりますが、std::variantに空の状態(1バイト)を追加した場合、その大きさは変わりません。入れるべき型が1バイト未満のものしかないということはまあないですし、何が入っているかはstd::size_tで管理しているので追加の管理コストもありません。

expected (P0323R3)

std::expected<T, E>は、期待する型の有効な値か、エラー型の値を持つ型です。

これは標準入りしているわけではありません。提案されただけです。ここで提案が見られます。なので、以降文法が変更される可能性がありますし、最悪入らないかもしれません。それゆえ深入りはしないことにしますが、アイデアの紹介はしておくべきだと判断しました。

これは、基本的にはstd::optionalですが、エラー時にはエラー型Eを保持することになります。なので、std::variant<T, E>と捉えることも可能でしょう。違いとしては、これはエラー処理に特化しているという点です。「無いかも知れない」std::optionalや、「この内のどれか」のstd::variantとはその点で違っています。std::expected<T, E>は、「成功すればT、失敗すればE」です。失敗の理由などを伝えることができる点がstd::optionalと異なり、状態に正しい方、エラーの方、という意味が付与されている点がstd::variantと異なります。

例:失敗するかもしれない関数

提案からユースケースを引用して来ましょう。ゼロ除算が起きたことをエラーとして伝えたい場合、これまでは専用の例外型を投げたりしていました。

struct DivideByZero : public std::exception { ... };
double safe_divide(double i, double j)
{
    if(j == 0) throw DivideByZero();
    else return i / j
}

std::expectedstd::errcを使えば、例えば以下のようなエラーコードを先に用意しておき、

enum class arithmetic_errc
{
    divide_by_zero, // 9 / 0 == ?
    not_integer_division, // 5 / 2 == 2.5 (which is not an integer)
    integer_divide_overflows, // INT_MIN / -1
};

それを持たせて返すことが可能です。

expected<double, errc> safe_divide(double i, double j)
{
    if (j == 0) return unexpected(arithmetic_errc::divide_by_zero);
    else return i / j;
}

値の取り出し方は、似た用途として使われているstd::optionalに揃えられています。

expected<double, errc> f1(double i, double j, double k)
{
    auto q = safe_divide(j, k);
    if(q) return i + *q;
    else return q;
}

エラーを取り出す方法も似ています。

int divide2(int i, int j)
{
    auto e = safe_divide(i, j);
    if(!e)
        switch(e.error().value()) {
        case arithmetic_errc::divide_by_zero: return 0;
        case arithmetic_errc::not_integer_division: return i / j; // Ignore.
        case arithmetic_errc::integer_divide_overflows: return INT_MIN;
        // No default! Adding a new enum value causes a compiler warning here,
        // forcing an update of the code.
        }
    return *e;
}

失敗するかもしれないもの、例えば文字列をパースして整数を取り出す、というような処理は、std::optionalでもできますが、エラーの理由を伝えられる(文字列のおかしい部分を示す、など)点が便利な点です。std::variantを使う場合、どちらがerror型かということをユーザーが覚えていなければなりませんし、統一しなければなりません。std::expectedならそれらの手間は省けます。

Phantom type

Phantom type、幽霊型は、定義に現れないパラメータ型を持つ型です。テンプレートに取った型が、実際には使われません。この型は、値やメモリ領域に何も追加しないままに、コンパイルにおける型チェック時にだけ現れ、実行時には消え失せるようなパラメータを追加します。

それはどういうことでしょう? 「起き得ない状態を起き得なくする」という観点から、これはどういう状況で使えるのでしょう。

例:処理順序のチェック

例えば、プログラムが入力を受け付けるとします。その入力は大抵の場合、正しい文法または値を持っているかどうか、処理が進行する前にプログラム側にチェックされなければならないでしょう。チェック以外にも、ある程度前処理をするかもしれません。字句解析とかですね。

const std::string input = read_input_data();
const std::optional<std::string> sanitized = diagnosis(input);
if(preprocessed.has_value())
    execute_input_command(sanitized.value());
else
    print_error("invalid input: %1%", input);

これもいいですが、少し不安な部分があります。というのも、execute_input_commandはただのstd::stringを受け取るので、diagnosisを通さなくても実行できる、ということです。これはできれば忘れないでほしいのですが、人間は忘れる生き物です。長いコードを書きながら、全てを頭にしまっておく術はありません。

型のレベルでロジックを表現すれば、コンパイラは我々のミスを指摘してくれます。以下のような型を使いましょう。

template<typename tagT>
struct phantom{std::string value;};

struct raw{};
struct sanitized{};

using raw_input_string = phantom<raw>;
using sanitized_string = phantom<sanitized>;

raw_input_string read_input();
std::optional<sanitized_string> diagnosis(const raw_input_string&);
void execute_input_command(const sanitized_string&);

今、2つの同じ型の値があり、しかしコードの中ではその2つの意味合いは異なっているわけです。ここでやっているのは、値を変えないままに型の部分にだけパラメータを設定し、診断前の値が実行されてしまうことを防ぐということです。

コードは同様のままです(型推論さまさまですね!)。

const auto input = read_input_data();
const auto valid = diagnosis(input);
if(valid.has_value())
    execute_input_command(valid.value());
else
    print_error("invalid input: %1%", input);

ここでread_input_dataの返却値をそのままexecute_input_commandに渡すと、型が異なるのでエラーになります。execute_input_commandsanitized_stringを受け取るのであって、raw_input_stirngは受け取りません。

ここではだいぶ簡略化していますが、sanitized_stringのコンストラクタを定義してraw_input_stringをそのまま(diagnosis関数を通さずに)使って構築できないようにしておくと、間違って入力を字句解析せずに実行してしまったりはしなくなります。

ここでphantomstd::stringを持っているだけで、実行時処理には何も追加されてはいません。つまり、コンパイラはこれらの型チェックに用いたタグや幽霊型を全て、実行時には消しされるということです。十分な最適化の後には、raw_input_stringsanitized_stringもただのstd::stringになっていることでしょう。

例:Boost.Unitによる次元解析

Boost.Unitは単位演算用のライブラリで、様々な値に単位を型パラメータとして与えることができます。また、コンパイル時の型チェックによって次元解析を行い、書いた数式が物理的に妥当かどうかチェックをしてくれます。

#include <boost/units/systems/si.hpp>
#include <boost/units/systems/si/io.hpp>

boost::units::quantity<boost::units::si::length> len = 3 * boost::units::si::meter;
std::cout << len << std::endl; // 3 m
// boost::units::quantity<boost::units::si::velocity> vel = len; // compilation error!
boost::units::quantity<boost::units::si::velocity> vel = len / boost::units::si::seconds;
std::cout << vel << std::endl; // 3 m s^-1

ここでは、いくつかのよく使う物理量の次元やよく使う単位間の比が先に定義されています。それらのパラメータは型のパラメータとしてコンパイル時にチェックされるので、次元が合わないような値を代入しようとすると失敗します。

例えば、velocitylengthを入れようとすると失敗しますが、lengthtimeで割ってからなら成功します。単位の違いによる比率の調節は実行時にも残りますが、次元解析の分の型パラメータは最適化によって実行時には姿を消します。

あとがき

「型によって意味を表現する」という観点からテクニックを紹介してきました。

折角コンパイラを使っているので、ミスはできるだけコンパイル時に発見したいです。コードがその意味をそのまま表すように、型を見れば何をするかわかるようにした方がコードもシンプルになるでしょう。そういった場合に、型によってロジックを実装していくことが強力なツールになり得るということが伝わっていれば嬉しいです。

References

特に、このような記事を書こうと漠然と思っていた時にBen Deane氏の素晴らしいトークを見て色々と固まったので、この記事は彼のトークから強い影響を受けています。

87
55
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
87
55