内部構造を知る操作と知らない操作
(言語は何でも良いが) Haskellでの以下2つのアプローチの比較
内部構造を知る操作
代数的データ型とパターンマッチを使用
内部構造を知らない操作
型クラスを使用
内部構造を知る操作
例
code:hs
data Shape = Circle Float | Rectangle Float Float
area :: Shape -> Float
area (Circle radius) = pi * radius * radius
area (Rectangle width height) = width * height
areaという関数は、Shapeの内部構造を知ったうえで処理の分岐をしている
すなわち、引数に取った値が、「CircleとRectangleから成る」という知識を持っている
このareaという操作は、Shapeと密に結合している
Shapeに新しいコンストラクタを追加するたびに、この関数も修正する必要がある
よく採用されるケースGPT-4.icon
内部構造が明確であり、複雑ではない場合
内部構造が単純で、変更が少ない場合、代数的データ型とパターンマッチは分かりやすく、明示的であるため好まれます。
e.g. 状態遷移モデル、抽象構文木(AST)など。
型の内部構造を操作する必要がある場合
特定のデータ型の内部構造に依存する処理を行う場合、パターンマッチが自然な選択です。
e.g. パーサ、コンパイラのフロントエンド、データの検証など。
内部構造を知らない操作
例
code:hs
class Shape a where
area :: a -> Float
data Circle = Circle Float
data Rectangle = Rectangle Float Float
instance Shape Circle where
area (Circle radius) = pi * radius * radius
instance Shape Rectangle where
area (Rectangle width height) = width * height
Shape型クラスは、それがareaという操作を定義している
areaという関数は、それが使われる具体的なデータ構造を知らない
CircleやRectangleという構造を知らない
また、areaを使う関数も結合が薄くなる
code:hs
f :: Shape a => a -> Int
この関数fも、CircleやRectangleという構造を知らない
Shapeに新しいコンストラクタを追加するためには、新しいインスタンスを定義するだけで済む
よく採用されるケースGPT-4.icon
多態性を持たせたい場合
異なる型に対して共通のインターフェースを提供し、多態性を持たせる場合、型クラスが適しています。
e.g. データベースアクセス層、シリアライズ/デシリアライズのインターフェースなど。
実装の隠蔽と拡張性を確保したい場合
型クラスを使うことで、具体的な型の実装を隠蔽し、拡張性を持たせることができます。
e.g. ログ出力、計算のアルゴリズム選択など。
モジュール内の話なら、前者で十分でしょう
いっぽうで、外部モジュールとやり取りする場合は、後者の方が望ましい
前者の場合
code:A.hs
-- A.hs
module A (Shape(..), area) where
data Shape = Circle Float | Rectangle Float Float
area :: Shape -> Float
area (Circle radius) = pi * radius * radius
area (Rectangle width height) = width * height
code:B.hs
-- B.hs
module B where
import A (Shape(..), area)
describeShape :: Shape -> String
describeShape shape =
"The area of the shape is " ++ show (area shape)
Bからの使われ方に着目する
describeShapeの引数はShapeという具体的なデータ型
後者の場合
code:A.hs
-- A.hs
module A (Shape(..), Circle(..), Rectangle(..)) where
class Shape a where
area :: a -> Float
data Circle = Circle Float
data Rectangle = Rectangle Float Float
instance Shape Circle where
area (Circle radius) = pi * radius * radius
instance Shape Rectangle where
area (Rectangle width height) = width * height
code:B.hs
-- B.hs
module B where
import A (Shape(..))
describeShape :: (Shape a) => a -> String
describeShape shape =
"The area of the shape is " ++ show (area shape)
Bからの使われ方に着目する
describeShapeの引数は(Shape a) => aという抽象的な操作に関する型
まあでも、ShapeのinstanceがAという1つのmodule内でしか使われてない前提なら大差ないかmrsekut.icon
違いがでてきそうなのは、
型クラスでmoduleをまたぐ汎用的な操作を提供し、
複数のmoduleの差異を閉じ込めたい時、と言えるだろうか
例えば、CRUD操作のようなものを考えると、saveみたいな操作が必要になる
これは、具体的なEntityであるUserとかPostとかいったものを横断して使用できる
この場合、CRUDクラスにさえ依存すれば良くて、
実際のロジックは、UserとかPostとかいった複数のmoduleをまたぐなにかになる
こういう例だと、嬉しいかも
特定のmoduleに依存しない、という大きなメリット享受できる
ということは、今まで1つに閉じていると思っていたものが、別のmoduleとみなすぐらい大きくなってきたら方針を変えるという感じだろうか
T = A|B|Cぐらいに思えていたものが、
個々のA,B,Cがかなり大きくなってきて、1つの抽象として表現しきれなくなった時に、
AとBを横断するUや、BとCを横断するWのように分けることになるのだろうか
Offeringは10個程度のmoduleを横断する抽象である
ちゃうか
htufの放火も
offeringもそうか
個々のorderlineとかを表示したい時に、
出す側としては個々のoffeirngの構造はどうでもいいが、
offering視点からは構造は知っている必要がある
基本的に、なにかと何かがやり取りするときって、
個々のデータ構造ってどうでも良くて、
共通するinterface(操作)が決まっていればそれで完結する
code:hs
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances #-} -- 支払い情報の型定義
data PaymentInfo = PaymentInfo {
amount :: Double,
currency :: String
} deriving Show
-- 型クラスの定義
class PaymentMethod p where
pay :: p -> PaymentInfo -> String
-- 具体的な支払い方法の型定義と型クラスインスタンスの定義
data CreditCard = CreditCard { cardNumber :: String, cardHolder :: String }
data BankTransfer = BankTransfer { accountNumber :: String, bankName :: String }
instance PaymentMethod CreditCard where
pay card info =
"Processing credit card payment for " ++ cardHolder card ++
", amount: " ++ show (amount info) ++ " " ++ currency info
instance PaymentMethod BankTransfer where
pay transfer info =
"Processing bank transfer to account " ++ accountNumber transfer ++
" at " ++ bankName transfer ++
", amount: " ++ show (amount info) ++ " " ++ currency info
-- 支払い方法に基づいて支払いを処理する関数
processAnyPayment :: (PaymentMethod p) => p -> PaymentInfo -> String
processAnyPayment method info = pay method info
-- 使用例
main :: IO ()
main = do
let paymentInfo = PaymentInfo { amount = 100.0, currency = "USD" }
let creditCard = CreditCard { cardNumber = "1234-5678-9876-5432", cardHolder = "John Doe" }
let bankTransfer = BankTransfer { accountNumber = "987654321", bankName = "Bank of Haskell" }
putStrLn $ processAnyPayment creditCard paymentInfo
putStrLn $ processAnyPayment bankTransfer paymentInfo
code:ts
// 支払い情報の型定義
interface PaymentInfo {
amount: number;
currency: string;
}
// 支払い方法インターフェースの定義
interface PaymentMethod {
pay(info: PaymentInfo): string;
}
// 具体的な支払い方法のクラス定義
class CreditCard implements PaymentMethod {
cardNumber: string;
cardHolder: string;
constructor(cardNumber: string, cardHolder: string) {
this.cardNumber = cardNumber;
this.cardHolder = cardHolder;
}
pay(info: PaymentInfo): string {
return Processing credit card payment for ${this.cardHolder}, amount: ${info.amount} ${info.currency};
}
}
class BankTransfer implements PaymentMethod {
accountNumber: string;
bankName: string;
constructor(accountNumber: string, bankName: string) {
this.accountNumber = accountNumber;
this.bankName = bankName;
}
pay(info: PaymentInfo): string {
return Processing bank transfer to account ${this.accountNumber} at ${this.bankName}, amount: ${info.amount} ${info.currency};
}
}
// 支払い方法に基づいて支払いを処理する関数
const pay = (method: PaymentMethod, info: PaymentInfo): string => {
return method.pay(info);
}
// 使用例
const paymentInfo: PaymentInfo = { amount: 100.0, currency: "USD" };
const creditCard = new CreditCard("1234-5678-9876-5432", "John Doe");
const bankTransfer = new BankTransfer("987654321", "Bank of TypeScript");
console.log(pay(creditCard, paymentInfo));
console.log(pay(bankTransfer, paymentInfo));
code:ts
// 支払い情報の型定義
interface PaymentInfo {
amount: number;
currency: string;
}
// 支払い方法の型定義
type PaymentMethod = CreditCardPayment | BankTransferPayment;
type CreditCardPayment = {
type: 'creditCard';
cardNumber: CardNumber;
cardHolder: CardHolder;
};
type BankTransferPayment = {
type: 'bankTransfer';
accountNumber: AccountNumber;
bankName: BankName;
};
// 支払い方法に基づいて支払いを処理する関数
const pay = (method: PaymentMethod, info: PaymentInfo): string => {
switch (method.type) {
case 'creditCard':
return Processing credit card payment for ${method.cardHolder}, amount: ${info.amount} ${info.currency};
case 'bankTransfer':
return Processing bank transfer to account ${method.accountNumber} at ${method.bankName}, amount: ${info.amount} ${info.currency};
default:
throw new Error('Unknown payment method');
}
}
// 使用例
const paymentInfo: PaymentInfo = { amount: 100.0, currency: "USD" };
const creditCard: PaymentMethod = { type: 'creditCard', cardNumber: "1234-5678-9876-5432", cardHolder: "John Doe" };
const bankTransfer: PaymentMethod = { type: 'bankTransfer', accountNumber: "987654321", bankName: "Bank of TypeScript" };
console.log(pay(creditCard, paymentInfo));
console.log(pay(bankTransfer, paymentInfo));
この部分
この2つは実現できている
code:hs
-- 支払い方法に基づいて支払いを処理する関数
processAnyPayment :: (PaymentMethod p) => p -> PaymentInfo -> String
processAnyPayment method info = processPayment method info
code:ts
interface PaymentMethod {
processPayment(info: PaymentInfo): string;
}
// 支払い方法に基づいて支払いを処理する関数
const processAnyPayment = (method: PaymentMethod, info: PaymentInfo): string => {
return method.processPayment(info);
}
PaymentMethodという操作のみを持った抽象に対して依存している
その中身(データ構造)がどういうものなのかは、この関数たちは知らない
これだと実現できていない
code:ts
interface PaymentMethod {
type: PaymentMethodType;
cardNumber?: string;
cardHolder?: string;
paypalId?: string;
accountNumber?: string;
bankName?: string;
}
// 支払い方法に基づいて支払いを処理する関数
const processAnyPayment = (method: PaymentMethod, info: PaymentInfo): string => {
switch (method.type) {
case 'creditCard':
return ...
case 'paypal':
return ...
case 'bankTransfer':
return ...
}
}
この関数の引数にあるPaymentMethodは具体的なデータ構造である
抽象(操作)ではない
具体的な構造に依存してしまう
具体的な構造を知らないと、ロジックを分岐できない