パターンマッチの構文が前置か後置か覚えられない人がやりがちなこと

突然思い出して随分前のネタを引っ張り出してきました.

OCamlScala のパターンマッチ構文は共に match というキーワードを使いますが, 前置 / 後置が異なるため, 交互に書いていると混乱して結構な頻度で間違えます.

let y = match x with
  | Some n -> n
  | None   -> 0
val y = x match {
  case Some(n) => n
  case None    => 0
}

あとごく稀に先に case 書いてしまったりする. これは完全に余談です.

y = case x of
  Just n -> n
  Nothing -> 0

話を戻して, 構文間違えて途中で気づいて書き直したりコンパイラくんに怒られるのも嫌なので, Atom の init script に以下のような感じのを書いて, Scala を書いているときに前置で match ... with と書くと勝手に後置に変換してくれるようにしています. case もよく書き忘れるのでついでに補完します.

"use babel";

import { CompositeDisposable } from "atom";

const matchWithRegex = /match(\W+.+?\W+)with/g;

atom.workspace.observeTextEditors(editor => {
  const subscriptions = new CompositeDisposable();
  subscriptions.add(editor.onDidDestroy(() => {
    subscriptions.dispose();
  }));
  subscriptions.add(editor.onDidInsertText(() => {
    const pos = editor.getCursorBufferPosition();
    const scopes = editor.scopeDescriptorForBufferPosition(pos).getScopesArray();
    if (!scopes.includes("source.scala") || scopes.findIndex(scope => /string|comment/.test(scope)) >= 0) {
      return;
    }
    const scanRange = [[pos.row, 0], pos];
    editor.backwardsScanInBufferRange(matchWithRegex, scanRange, ({ match, range, stop, replace }) => {
      stop();
      const prevChar = editor.getTextInBufferRange([range.start.translate([0, -1]), range.start]);
      if (!range.end.isEqual(pos) || /\w/.test(prevChar)) {
        return;
      }
      editor.transact(() => {
        const replaceText = `${match[1].trim()} match {}`;
        replace(replaceText);
        editor.setCursorBufferPosition(range.start.translate([0, replaceText.length - 1]));
        editor.insertNewline();
        editor.insertText("case");
      });
    });
  }));
});

こんな感じ.

最近は OCaml 書いていないので直接後置で書ける確率が高くなってる.

Q. まだ Atom をつかっているのですか?

はい.

subtype 多相, never, union を有効活用していこうな

まずは MonadPlus ばんじゃーいとこういう感じに mzeromplus を定義してみる. mzero が関数になっているのは TypeScript ではこうしないと (関数以外の値を) パラメータ多相にできないため. ちなみに Parser<A>A は結果の値の型で, Parser はこれについて共変.

declare const mzero: <A>() => Parser<A>;
declare const mplus: <A>(parserA: Parser<A>, parserB: Parser<A>) => Parser<A>;

するとこういう感じで, 本当は p2Parser<T> になってほしいのだけれど, 明示的に型パラメータを渡したり注釈を書かない限りは mzero を適当にインスタンス化するのが優先されて Parser<{}> になってしまう.

enum T { } // なんでも良い
declare const parser: Parser<T>;

const p1 = mplus(parser, mzero()); // : Parser<T>
const p2 = mplus(mzero(), parser); // : Parser<{}> ← えっあのちょっと

というわけでパラメータ多相をやめて, かわりに subtype 多相を使ってこんなふうに never と union (|) を使って定義しなおす.

declare const mzero: Parser<never>;
declare const mplus: <A, B>(parserA: Parser<A>, parserB: Parser<B>) => Parser<A | B>;

するとめでたく両方 Parser<T> になります. 任意の T に対して T | never = never | T = T なので never は消えてなくなる. ついでに mzero を使うのにいちいち関数呼び出しをする必要もなくなって便利.

const p1 = mplus(parser, mzero); // : Parser<T>
const p2 = mplus(mzero, parser); // : Parser<T>

かわりに mplus の引数として別々の型を返す Parser を渡して良くなってしまっているわけですが, だからといって結果の型に不整合が出たりするわけではないので特に困ることはないでしょう.

何の話: https://github.com/susisu/loquat/blob/master/packages/simple/index.d.ts