関数型デザイン9章のコードをTypeScriptで書けない
(もちろんclassを使えば書ける)
『関数型デザイン 原則、パターン、実践』.iconの11章の内容から冗長な部分を取り除いて、
TypeScript, Clojure, Haskellの3つでFP志向で書く
Clojureは別にいらんけど、本がClojureなので一応残しておく
構成としては3つの登場人物がある
理想的には、こういう構成にしたい
https://gyazo.com/e8b470b8fcddb0d4529b2e244f0a8e46
例えば、こういう構成に拡張できる
https://gyazo.com/7955586af0cf1ad186e8a4daab98dd25
ポイントとしては、
PayrollInterfaceは抽象なので何にも依存しない
Payrollは、PayrollInterfaceに依存する
interfaceだけを見て、実装の詳細は知らない
PayrollImplementationに実装の詳細を定義する
ここで、
Payroll→PayrollInterface←PayrollImplementation
以下のコードでは、HaskellとClojureではそれが実現できているが、TypeScriptでは実現できていない
普通に書くと、TypeScriptでは、以下のように書いて、Interfaceが実装の詳細に依存しないと実現できない
code:PayrollInterface.ts
import {...} from 'PayrollImplementaiton'
しかし、これは「Interfaceは他のものに依存しない」というルールを違反してしまう
hs
code:Payroll.hs
module Payroll where
import PayrollInterface
payroll :: CalcPay a => a -> Double payroll employees = map calcPay employees
code:PayrollInterface.hs
module PayrollInterface where
class CalcPay a where
calcPay :: a -> Double
code:PayrollImplementation.hs
module PayrollImplementation where
import PayrollInterface
data Employee = Salaried { salary :: Double }
| Hourly { hoursWorked :: Double, hourlyRate :: Double }
| Commissioned { basePay :: Double, sales :: Double, commissionRate :: Double }
instance CalcPay Employee where
calcPay (Salaried salary) = salary
calcPay (Hourly hoursWorked hourlyRate) = hoursWorked * hourlyRate
calcPay (Commissioned basePay sales commissionRate) = basePay + (sales * commissionRate)
ts
code:Payroll.ts
import { calcPay, Employee } from './PayrollInterface';
export function payroll(employees: Employee[]): number[] {
return employees.map(calcPay);
}
code:PayrollInterface.ts
export type Employee =
| { employmentType: "salaried"; salary: number }
| { employmentType: "hourly"; hoursWorked: number; hourlyRate: number }
| {
employmentType: "commissioned";
basePay: number;
sales: number;
commissionRate: number;
};
export interface CalcPay {
(employee: Employee): number;
}
export let calcPay: CalcPay;
export function setCalcPay(func: CalcPay) {
calcPay = func;
}
code:PayrollImplementation.ts
// PayrollImplementation.ts
import { Employee, setCalcPay } from './PayrollInterface';
function calcPayImpl(employee: Employee) {
switch (employee.employmentType) {
case 'salaried':
return getSalary(employee);
case 'hourly':
return calcHourlyPay(employee);
case 'commissioned':
return calcCommissionedPay(employee);
default:
throw new Error(Unknown employment type: ${employee.employmentType});
}
}
function getSalary(employee: { employmentType: 'salaried'; salary: number }) {
return employee.salary;
}
function calcHourlyPay(employee: { employmentType: 'hourly'; hoursWorked: number; hourlyRate: number }) {
return employee.hoursWorked * employee.hourlyRate;
}
function calcCommissionedPay(employee: { employmentType: 'commissioned'; basePay: number; sales: number; commissionRate: number }) {
return employee.basePay + (employee.sales * employee.commissionRate);
}
// Set the implementation
setCalcPay(calcPayImpl);
setClacPayはGPT-4.iconが発案したもの
キモいけど、こうすれば確かに実現はできる?
別に、その形だけが正解というわけではないのだが
そうだとして、別の方法での良い実現方法を模索しないといけない
最終的にやりたいことは、
Payroll自体はinterfaceにのみ依存しており、
別の箇所から詳細を注入できれば良い
clj
code:Payroll.clj
(ns payroll
(:require [payroll-interface :refer calc-pay])) (map calc-pay employees))
code:PayrollInterface.clj
(ns payroll-interface)
(defmulti calc-pay :employment-type)
(throw (IllegalArgumentException. (str "Unknown employment type: " (:employment-type employee)))))
code:PayrollImplementation.clj
(ns payroll-implementation
(:require [payroll-interface :refer calc-pay])) (:salary employee))
(* (:hours-worked employee) (:hourly-rate employee)))
(defmethod calc-pay :commissioned employee (+ (:base-pay employee) (* (:sales employee) (:commission-rate employee))))