andre-taissin-hOwcob_3dpc-unsplash.jpg

fp-tsとOrd

 
0
このエントリーをはてなブックマークに追加
Kazuki Moriyama
Kazuki Moriyama (森山 和樹)

順番を司る型クラスOrdの紹介。

https://gcanti.github.io/fp-ts/modules/Ord.ts.html

定義

export interface Ord<A> extends Eq<A> {
  readonly compare: (first: A, second: A) => Ordering
}

compareという比較用の関数が一つだけ生えている。
compareが返すOrderingは比較結果を表す-1、0、1のどれかである。
firstがsecondよりも小さい場合は-1を、等値な場合は0を、大きい場合は1を返す。

使い方

import * as Ord from "fp-ts/Ord";
import * as S from "fp-ts/string";
import * as N from "fp-ts/number";

console.log(N.Ord.compare(1, 2)); // => -1
console.log(N.Ord.compare(1, 1)); // => 0
console.log(S.Ord.compare("a", "b")); // => -1

法則

Ordは次の3つのlawを満足する必要がある。

  1. 反射律: compare(a, a) <= 0
  2. 反対称律: compare(a, b) <= 0 かつ compare(b, a) <= 0 ならば a = b
  3. 推移律: compare(a, b) <= 0 かつ compare(b, c) <= 0 ならば compare(a, c) <= 0

combinators

contramap

Ord<A>とBをAに変換する関数B => AからOrd<B>を作成する。

console.log(Ord.contramap((s: string) => s.length)(N.Ord).compare("a", "b")); // => 0

reverse

結果として出てきたOrderingをひっくり返す。

console.log(Ord.reverse(N.Ord).compare(1, 2)); // => 1

tuple

複数のOrdインスタンスからタプルのOrdインスタンスを作成する。

const tuple3Ord = Ord.tuple(S.Ord, N.Ord);

console.log(tuple3Ord.compare(["a", 2], ["b", 1])); // => -1
console.log(tuple3Ord.compare(["a", 2], ["a", 1])); // => 1

constructors

fromCompare

比較関数からOrdインスタンスを作成する。
インスタンス作成のヘルパーファクトリー関数。
注意すべきは比較関数はOrdering(= -1|0|1)を返す必要がある。

const lenStrOrd = Ord.fromCompare<string>((a, b) => {
  if (a.length === b.length) {
    return 0;
  } else if (a.length < b.length) {
    return -1;
  } else {
    return 1;
  }
});

console.log(lenStrOrd.compare("a", "b")); // => 0
console.log(lenStrOrd.compare("ab", "b")); // => 1

equalsDefault

Orderingを返すような比較関数をとって、二値が等しいかをbooleanで返す関数を返す。
まず二値が===で比較され等しくない場合にのみ渡された比較関数での等値性検証が行われる。

console.log(Ord.equalsDefault(lenStrOrd.compare)("a", "b")); // => true
console.log(Ord.equalsDefault(lenStrOrd.compare)("a", "a")); // => true

// イメージはこんな感じ
// first === second || compare(first, second) === 0;

instances

Contravariant

OrdのContravariantインスタンス。

console.log(
  Ord.Contravariant.contramap(N.Ord, (s: string) => s.length).compare("a", "b")
); // => 0

getMonoid

Monoid<Ord<A>>を取得する。
Monoid.concat(ord1, ord2)はまずord1で比較し、等値な場合にのみord2で比較するようなOrdを返す。
Monoid.emptyは常に0を返すようなOrdである。

const numOrdMon = Ord.getMonoid<number>();

console.log(numOrdMon.empty.compare(1, 2)); // => 0

console.log(
  numOrdMon
    .concat(
      N.Ord,
      Ord.fromCompare<number>(() => 1)
    )
    .compare(0, 0)
); // => 0

getSemigroup

getMonoidのemptyがなくなったバージョン。

utils

between

特定区間に収まっているかを判定する。

console.log(Ord.between(N.Ord)(0, 10)(3)); // => true
console.log(Ord.between(N.Ord)(0, 10)(0)); // => true

clamp

値を特定区間に押し込める。

console.log(Ord.clamp(N.Ord)(0, 10)(3)); // => 3
console.log(Ord.clamp(N.Ord)(0, 10)(11)); // => 10
console.log(Ord.clamp(N.Ord)(0, 10)(-1)); // => 0

geq/gt/leq/lt

よくある大なり・小なり・以上・以下の比較関数。

console.log(Ord.geq(N.Ord)(2, 1)); // => true
console.log(Ord.geq(N.Ord)(1, 1)); // => true

console.log(Ord.gt(N.Ord)(2, 1)); // => true
console.log(Ord.gt(N.Ord)(1, 1)); // => false

console.log(Ord.leq(N.Ord)(1, 1)); // => true
console.log(Ord.leq(N.Ord)(1, 2)); // => true

console.log(Ord.lt(N.Ord)(1, 1)); // => false
console.log(Ord.lt(N.Ord)(1, 2)); // => true

max/min

大きい方、小さい方を取得する。

console.log(Ord.max(N.Ord)(1, 2)); // => 2
console.log(Ord.min(N.Ord)(1, 2)); // => 1

オブジェクトに対するOrdを作成する

Eq.structのようにオブジェクトに対して簡易的にOrdを作成する関数は用意されていない。
ここではMonoidを使ったオブジェクト用のOrd作成方法を紹介する。

まずは下のようなオブジェクトが存在するとする。

type Person = {
  name: string;
  age: number;
};

それぞれの要素で判断するOrdインスタンスを作成する。
その後OrdのMonoidで各要素のOrdを合成してすべての要素で比較するOrdを作成する。

const byNameOrd = Ord.contramap((p: Person) => p.name)(S.Ord);
const byAgeOrd = Ord.contramap((p: Person) => p.age)(N.Ord);
const personOrd = Ord.getMonoid<Person>().concat(byNameOrd, byAgeOrd);

このOrdを使えばPersonの要素すべてを気にしながらcompareできる。

console.log(
  personOrd.compare({ name: "taro", age: 20 }, { name: "taro", age: 20 })
); // => 0
console.log(
  personOrd.compare({ name: "taro", age: 10 }, { name: "taro", age: 20 })
); // => -1
console.log(
  personOrd.compare({ name: "jon", age: 20 }, { name: "taro", age: 20 })
); // => -1

注意すべきはMonoidのconcatのロジックによって先にbyNameでの比較が行われるということだ。
つまり、もしnameが異なるならばageが何であろうと名前比較の結果が全体の結果になってしまう。
ageでの比較が行われるのはname同士が等しいときのみである。

info-outline

お知らせ

K.DEVは株式会社KDOTにより運営されています。記事の内容や会社でのITに関わる一般的なご相談に専門の社員がお答えしております。ぜひお気軽にご連絡ください。