2011-12-22 6 views
15

고려 :이 같은 미친 물건 작동하는지 아주 오싹하지만Typeclass 인스턴스의 2 차 폭발을 피하는 방법은 무엇입니까?

{-# OPTIONS -fglasgow-exts #-} 

data Second = Second 
data Minute = Minute 
data Hour = Hour 

-- Look Ma', a phantom type! 
data Time a = Time Int 

instance Show (Time Second) where 
    show (Time t) = show t ++ "sec" 

instance Show (Time Minute) where 
    show (Time t) = show t ++ "min" 

instance Show (Time Hour) where 
    show (Time t) = show t ++ "hrs" 

sec :: Int -> Time Second 
sec t = Time t 

minute :: Int -> Time Minute 
minute t = Time t 

hour :: Int -> Time Hour 
hour t = Time t 

class TimeAdder a b c | a b -> c where 
    add :: Time a -> Time b -> Time c 

instance TimeAdder Second Second Second where 
    add (Time s1) (Time s2) = sec (s1 + s2) 

instance TimeAdder Second Minute Second where 
    add (Time s) (Time m) = sec (s + 60*m) 

instance TimeAdder Second Hour Second where 
    add (Time s) (Time h) = sec (s + 3600*h) 

instance TimeAdder Minute Second Second where 
    add (Time m) (Time s) = sec (60*m + s) 

instance TimeAdder Minute Minute Minute where 
    add (Time m1) (Time m2) = minute (m1 + m2) 

instance TimeAdder Minute Hour Minute where 
    add (Time m) (Time h) = minute (m + 60*h) 

instance TimeAdder Hour Second Second where 
    add (Time h) (Time s) = sec (3600*h + s) 

instance TimeAdder Hour Minute Minute where 
    add (Time h) (Time m) = minute (60*h + m) 

instance TimeAdder Hour Hour Hour where 
    add (Time h1) (Time h2) = hour (h1 + h2) 

add (minute 5) (hour 2) 
--125min 

, 나는 TimeAdder 인스턴스의 차 폭발을 피할 수있는 방법 궁금합니다.

+1

시간, 분 및 초가 실제로 이런 종류의 유형 안전을 제공하는 후보가 아닙니다. 초 단위의 시간 만 허용합니까? 이런 종류의 유형 안전을위한 더 좋은 운동은 예를 들어 물리적 단위 일 수 있습니다. 여러분은'Time','Mass','Length' 등을 유령 타입으로하고 속도, 에너지 등에 대한 타입 안전 계산을 할 수 있습니다. 이것은 모든 타입이 너의 시간 예. – shang

+0

@shang : 정확합니다. 나는 이것이 타입 클래스와 팬텀 타입에 대한 더 나은 핸들을 얻기위한 장난감의 예일 뿐이라고 언급 했어야 만한다. hammar의 첫 번째 대답은 시간 단위가있는 실제 응용 프로그램의 경우 훨씬 더 실용적입니다. – Landei

답변

9

당신은 기능상의 의존성을 부여하지 않습니다.

class TimeUnit a where 
    toSeconds :: a -> Int 
    fromSeconds :: Int -> a 

instance TimeUnit (Time Second) where toSeconds = id; fromSeconds = id 
instance TimeUnit (Time Minute) where toSeconds = (* 60); fromSeconds = (`quot` 60) 

class TimeAdd a b c where 
    add :: a -> b -> c 

instance (TimeUnit a, TimeUnit b, TimeUnit c) => TimeAdd a b c where 
    add a b = fromSeconds (toSeconds a + toSeconds b) 
13

당신은, 난 그냥 평범한 구식 ADT를 유형의 클래스를 생략하고 사용하는 것이 좋은 이유가없는 한이 특정 단순화 할 수 있다는 장점이있다

data Time = Hour Int | Minute Int | Second Int 

instance Show Time where 
    show (Hour x) = show x ++ "hrs" 
    show (Minute x) = show x ++ "min" 
    show (Second x) = show x ++ "sec" 

add x y = fromSeconds (toSeconds x + toSeconds y) 

toSeconds (Hour x) = 3600 * x 
toSeconds (Minute x) = 60 * x 
toSeconds (Second x) = x 

fromSeconds x | mod x 3600 == 0 = Hour (div x 3600) 
       | mod x 60 == 0 = Minute (div x 60) 
       | otherwise = Second x 

유형 클래스 접근 할 수있는 그 예를 들면 다음과 같습니다.

> add (Second 18) (Second 42) 
1min 
+0

좋은 지적! 나는 실제 응용 프로그램을 위해 그렇게 할 것입니다. 그러나 필자는 필자의 예에서 팬텀 유형을 더 잘 이해하려고 노력했다. – Landei

+5

hammar의 코드를 타입 클래스로 다시 변환 할 수 있으며 초 단위를 중간 단위로 사용하기 때문에 이차 폭발이 발생하지 않습니다. –

+1

@SjoerdVisscher 제발 좀 더 자세히 설명해 주시겠습니까? 반환 형식에서 올바른 팬텀 형식을 ​​얻는 방법을 알지 못합니다. – dave4420

0

인스턴스는 모두 상용구입니다. Template Haskell에 대한 사례라고 말할 수 있습니다 (분노로 사용하는 사람에게 그렇게하는 방법에 대한 설명은 남겨 두겠습니다).

1

hammar 님의 제안을 한 걸음 더 나아가서,이 특별한 예를 들어 설명해 드리겠습니다. 유형 항목을 모두 제거하고 대신 스마트 생성자를 사용하십시오.

newtype Time = Sec Int 

instance Show Time where 
    show (Sec n) = h ++ " hrs " ++ m ++ " min " ++ s ++ " sec" 
    where h = ... 
      m = ... 
      s = ... 

sec :: Int -> Time 
sec = Sec 

min :: Int -> Time 
min = sec . (*60) 

hr :: Int -> Time 
hr = min . (*60) 

add (Sec n) (Sec m) = Sec (n+m) 

물론 팬텀 유형이 없으므로 재미가 없습니다. 재미있는 운동 : hr, min, sec에 대한 렌즈를 만듭니다.

6

유형 수준에서이 작업을 수행하는 방법은 유령 유형을 유형 자연 수에 매핑하고 "최소"조작을 사용하여 올바른 리턴 유형을 찾은 다음 인스턴스 해결로 거기에서 작업을 수행하게하는 것입니다.

여기에 유형 패밀리가 사용되지만, 선호하는 경우 기능 종속성을 사용하여 수행 할 수 있습니다.

{-# LANGUAGE TypeFamilies, EmptyDataDecls, FlexibleInstances #-} 

먼저 유형 수준의 고유 기능과 최소 작동이 필요합니다.

data Zero 
data Succ n 

type family Min a b 
type instance Min Zero a = Zero 
type instance Min a Zero = Zero 
type instance Min (Succ a) (Succ b) = Succ (Min a b) 

다음으로, 우리는 우리의 팬텀 유형을 정의하고 우리 식 레벨 원주민에서 매핑을 제공합니다 :

data Second 
data Minute 
data Hour 

type family ToIndex a 
type instance ToIndex Hour = Succ (Succ Zero) 
type instance ToIndex Minute = Succ Zero 
type instance ToIndex Second = Zero 

type family FromIndex a 
type instance FromIndex (Succ (Succ Zero)) = Hour 
type instance FromIndex (Succ Zero) = Minute 
type instance FromIndex Zero = Second 

다음, Time 유형과 Show 인스턴스를. 이는 원래 코드에서와 동일합니다.

class Seconds a where 
    toSeconds :: Time a -> Int 
    fromSeconds :: Int -> Time a 

instance Seconds Hour where 
    toSeconds (Time x) = 3600 * x 
    fromSeconds x = Time $ x `div` 3600 

instance Seconds Minute where 
    toSeconds (Time x) = 60 * x 
    fromSeconds x = Time $ x `div` 60 

instance Seconds Second where 
    toSeconds (Time x) = x 
    fromSeconds x = Time x 

이제 남은 것은이 add 함수를 정의하는 것입니다

data Time a = Time Int 

instance Show (Time Second) where 
    show (Time t) = show t ++ "sec" 

instance Show (Time Minute) where 
    show (Time t) = show t ++ "min" 

instance Show (Time Hour) where 
    show (Time t) = show t ++ "hrs" 

sec :: Int -> Time Second 
sec t = Time t 

minute :: Int -> Time Minute 
minute t = Time t 

hour :: Int -> Time Hour 
hour t = Time t 

그냥 내 ADT 대답에, 우리는 중간 단위로 초를 사용합니다 좋아합니다.

add :: (Seconds a, Seconds b, Seconds c, 
     c ~ FromIndex (Min (ToIndex a) (ToIndex b))) 
     => Time a -> Time b -> Time c 
add x y = fromSeconds (toSeconds x + toSeconds y) 

마술은 올바른 반환 유형이 선택되도록하는 유형 동등 제약 조건에서 발생합니다.

이 코드는 당신이 원하는처럼 사용할 수 있습니다,

> add (minute 5) (hour 2) 
125min 

다른 장치를 추가하려면 Days 말, 당신은 단지 우리가 성공적으로 피할 것, 즉 Show, FromIndex, ToIndexSeconds에 대한 인스턴스를 추가해야 2 차 폭발. 인스턴스화 유형에 대한 제한은 그들이 형태의 것이 있기 때문에

+0

와우, 그건 정말 흥미 롭습니다. – Landei

2

첫 번째 부분은, 하스켈 2010 년에이 방법을 수행 할 수없는 T1 ... TN은 다른 유형의 변수와이 있음을

T t1 ... tn 

최대 하나의 인스턴스 프로 유형 및 클래스. Frege에서는 형식 형식에 대한 제한이 조금씩 해제되지만 중요한 제한은 클래스 당 최대 한 인스턴스 인 이고생성자을 입력합니다.

module Test where 

data Seconds = Seconds 
data Minutes = Minutes 
data Hours = Hours 

data Time u = Time Int 

class TimeUnit u where 
    verbose :: u -> String 
    fromTime :: Time u -> u 

instance TimeUnit Seconds where 
    verbose _ = "sec" 
    fromTime _ = Seconds 
instance TimeUnit Minutes where 
    verbose _ = "min" 
    fromTime _ = Minutes 
instance TimeUnit Hours where 
    verbose _ = "hrs" 
    fromTime _ = Hours 

instance Show (TimeUnit u) => Time u where 
    show ([email protected] t) = t.show ++ verbose (fromTime o) 

main _ = do 
println (Time 42 :: Time Seconds) 
println (Time 42 :: Time Minutes) 
println (Time 42 :: Time Hours) 

fromTime 응용 프로그램이 TimeUnit와 값이 전 nihilo 할 수 있도록 적절한 사전을 구성하기 위해 호출 사이트를 강제 정도가 나타납니다 여기 그럼에도 불구하고 쇼 파트를 할 수있는 방법이다.

가장 작은 단위로 계산을 가능하게하는 요인을 만들어 동일한 기술을 사용하여 다른 시간 형식간에 산술을 수행 할 수 있습니다.