Home About Contact
Haskell , TypeScript

Haskell, data を定義して使う、それを TypeScript に移植する

「(本)すごい Haskell たのしく学ぼう」 の7章に出ているデータ型の例を TypeScript に移植する。

Haskell 版 data Shape

環境はこれ:

$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 9.0.2

例として出ていた Shape データ型の定義:

data Shape
  = Circle Float Float Float
  | Rectangle Float Float Float Float
  deriving (Show)

面積を計算する area 関数を定義:

area :: Shape -> Float
area (Circle _ _ r) = pi * r ^ 2
area (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)

これらを使って書いたコード main.hs 全体:

data Shape
  = Circle Float Float Float
  | Rectangle Float Float Float Float
  deriving (Show)

area :: Shape -> Float
area (Circle _ _ r) = pi * r ^ 2
area (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)

main :: IO ()
main =
  putStrLn $
  (show $ area (Circle 0 0 100)) ++
  ", " ++ (show $ area (Rectangle 0 0 100 100))

それでは実行:

$ ghc main.hs
$ ./main
31415.928, 10000.0

data Shape を TypeScript に移植

data Shape を TypeScript に:

type Circle = {
  x: number
  y: number
  r: number
}

type Rectangle = {
  x1: number
  y1: number
  x2: number
  y2: number
}

type Shape = Circle | Rectangle

ただ area 関数を移植しようとしてわかったのですが、 この定義では多少不都合があります。 あとで修正します。

area 関数を定義(これは作動しない):

const area = (s: Shape): number => {
  //switch ( typeof s) {
  switch (s) {
    case Circle:
      return Math.PI * Math.pow(s.r, 2) 
    case Rectangle:
      return Math.abs(s.x2 - s.x1) * Math.abs(s.y2 - s.y1)
  }
}

Shape を受け取ってそれが Circle か Rectangle かを判別して、 それぞれの面積計算をして結果を返す関数です。

これで作動すればうれしいのですが、TypeScript では このような記述はできないようです。

TypeScript の実行環境はこれ:

$ deno --version
deno 2.5.2 (stable, release, aarch64-apple-darwin)
v8 14.0.365.5-rusty
typescript 5.9.2

Gemni にたずねたところ、 in を使って Circle か Rectangle か どちらかを調べる方法もあるけど、非推奨だと言われた。 そのかわり、Circle と Rectangle の定義に kind を追加して、 それで判別せよ、とのこと。

つまり、このように定義する:

type Circle = {
  kind: 'circle'
  x: number
  y: number
  r: number
}

type Rectangle = {
  kind: 'rectangle'
  x1: number
  y1: number
  x2: number
  y2: number
}

そうしておけば、 area 関数では kind を使って判定できる、このように:

const area = (s: Shape): number => {
  switch (s.kind) {
    case 'circle':
      return Math.PI * Math.pow(s.r, 2) 
    case 'rectangle':
      return Math.abs(s.x2 - s.x1) * Math.abs(s.y2 - s.y1)
  }
}

自明な定義を手動で追加するのは面倒ですが、致し方ない。

コード全体はこうなりました:

// main.ts

type Circle = {
  kind: 'circle'
  x: number
  y: number
  r: number
}

type Rectangle = {
  kind: 'rectangle'
  x1: number
  y1: number
  x2: number
  y2: number
}

type Shape = Circle | Rectangle


const area = (s: Shape): number => {
  switch (s.kind) {
    case 'circle':
      return Math.PI * Math.pow(s.r, 2) 
    case 'rectangle':
      return Math.abs(s.x2 - s.x1) * Math.abs(s.y2 - s.y1)
  }
}

const c: Circle = {
  kind: 'circle',
  x: 0,
  y: 0,
  r: 100
}

const r: Rectangle = {
  kind: 'rectangle',
  x1: 0,
  y1: 0,
  x2: 100,
  y2: 100
}

console.log(`${area(c)}, ${area(r)}`)

実行します:

$ deno run --check main.ts
Check file:///path/to/main.ts
31415.926535897932, 10000

できました。