一.ZipList 와 List
List 장면에서, xs <*> ys 는 좌측 xs 에서 함수를 꺼내어 우측 ys 의 각 항에 작용시키는 것을 나타내며, 2 가지 구현 방식이 있습니다:
-
데카르트 곱
-
지퍼식 일대일 페어링
각각 [] 와 ZipList 에 대응합니다. 예를 들어:
import Control.Applicative;
-- 데카르트 곱
> [(+2), (*2), (/2)] <*> [1, 2, 3]
[3.0,4.0,5.0,2.0,4.0,6.0,0.5,1.0,1.5]
-- 지퍼식 페어링
> getZipList $ ZipList [(+2), (*2)] <*> ZipList [1..]
[3,4]
데카르트 곱은 유한장의 List 에만 사용할 수 있으며, 지퍼식 페어링은 무한장의 List 장면에도 적용할 수 있습니다. <*> 而言, 이 2 가지 구현은 모두 허용되지만, [] 는 동시에 2 가지의 다른 Applicative 구현을 가질 수 없으므로, ZipList 를 만들어내고, 지퍼페어링 방식으로 Applicative 를 구현하게 합니다
P.S.여기서 언급한 <*> 는 Applicative 클래스가 정의하는 동작으로, 상세는 [Functor 와 Applicative_Haskell 노트 7](/articles/functor 与 applicative-haskell 笔记 7/) 참조
二.newtype
ZipList 는 이 장면을 위해 태어난 것으로, 본질적으로는 List 의 래핑입니다. 정의는 다음과 같습니다:
newtype ZipList a = ZipList { getZipList :: [a] }
deriving ( Show, Eq, Ord, Read, Functor
, Foldable, Generic, Generic1)
(Control.Applicative 에서 인용)
newtype 키워드를 통해, 기존 타입 ([]) 에 기반하여 새로운 (ZipList) 을 생성하고, 그 인터페이스 구현을 다시 작성합니다:
instance Applicative ZipList where
pure x = ZipList (repeat x)
liftA2 f (ZipList xs) (ZipList ys) = ZipList (zipWith f xs ys)
P.S.여기서는 liftA2 만을 구현하고, <*> 는 나타나지 않았습니다. Applicative 에는 최소 완전 정의 (minimal complete definition) 의 제약이 있기 때문입니다:
A minimal complete definition must include implementations of pure and of either <*> or liftA2. If it defines both, then they must behave the same as their default definitions:
(<*>) = liftA2 idliftA2 f x y = f <$> x <*> y
이 2 개의 함수의 관련이 사전에 정의되어 있으므로, 둘 중 하나만 구현하면 충분합니다 (관련 관계에 기반하여 자동으로 다른 하나를 생성할 수 있습니다)
그렇다면, newtype 은 실제로 무엇을 수행했을까요?
실제로, newtype 이 수행하는 것은새로운 타입을 생성하고, 기존 타입을 래핑하는 것뿐입니다
유사한 장면에서는, JS 의 경우, 이렇게 합니다:
class ThisType {
constructor(value) {
this.value = value;
}
['<*>']() {
console.log('데카르트 곱');
}
};
class ThatType {
constructor(...args) {
this.originalValue = new ThisType(...args);
}
getOriginalType() {
return this.originalValue;
}
['<*>']() {
console.log('지퍼페어링');
}
};
// test
let thisOne = new ThisType(1);
thisOne['<*>'](); // 데카르트 곱
console.log(thisOne); // ThisType?{value: 1}
let thatOne = new ThatType(2);
thatOne['<*>'](); // 지퍼페어링
console.log(thatOne.getOriginalType()); // ThisType?{value: 2}
새로운 타입 (ThatType) 을 생성하고, 원래 타입 (ThisType) 을 래핑하며, 다른 <*> 구현을 제공합니다
양자는 단순한 의존 관계이며, 상속 관계가 아니므로, newtype 으로 생성된 타입은 자동으로 원래 타입의 모든 메소드를 가지지 않습니다 (원래 타입이 구현한 typeclass 도 자동으로 획득하지 않습니다). 타입而言, 양자는 완전히 독립된 다른 타입입니다. 따라서:
> [3] ++ [1, 2]
[3,1,2]
> type IntList = [Int]
> [3] ++ ([1, 2] :: IntList)
[3,1,2]
> (ZipList [3]) ++ (ZipList [1, 2])
<interactive>:109:1: error:
? Couldn't match expected type '[a]'
with actual type 'ZipList Integer'
? In the first argument of '(++)', namely 'ZipList [3]'
...
type 으로 생성된별명 타입 은 원래 타입과 등가로 교환 사용할 수 있지만, newtype 으로 생성된신 타입 은 원래 타입과는 완전히 다른 것으로, 유일한 연결은 신 타입 내부에서 실제로 조작하는 것은 원래 타입 (원래 타입 인스턴스의 참조를 보유하는 것을 통해) 이며, 이 방식을 통해 외층에서 원래 타입의 확장/강화를 실현합니다
구문 요구
구문 작용으로 보면, newtype 은 data 와 마찬가지로, 모두 새로운 타입을 생성하는 데 사용되지만, newtype 이 제한이 더 많습니다:
data can only be replaced with newtype if the type has exactly one constructor with exactly one field inside it.
newtype 선언의 타입은값 컨스트럭터가 1 개만 있으며,かつ이 값 컨스트럭터는 파라미터가 1 개만 이어야 합니다 (field). 이之外, data 키워드와는 거의 차이가 없습니다
P.S.값 컨스트럭터와 파라미터에 대해서는, [타입_Haskell 노트 3](/articles/类型-haskell 笔记 3/#articleHeader7) 참조
三.type 과 data 의 비교
| 키워드 | 작용 | ��용 장면 |
|---|---|---|
data | 자신의 (데이터) 타입을 정의 | 완전히 새로운 타입을 정의하고 싶은 경우 |
type | 기존 타입에 별명을 붙이며, 얻어진 것은 원래 타입과 완전히 등가로, 무조건 교환/혼용 가능 | 타입 시그니처를 더 명확하게 (의미적으로) 하고 싶은 경우 |
newtype | 기존 타입을 새로운 타입에 래핑하며, 얻어진 타입은 원래 타입과 다르며, 교환/혼용 불가 | 기존 타입에 다른 인터페이스 (typeclass) 구현을 갖게 하고 싶은 경우 |
四.newtype 과 지연 계산
Haskell 에서는 대부분의 계산은 지연적입니다 (소수는 foldl', Data.ByteString 등의 엄격판을 가리킵니다). 즉, 계산은不得不算의 때에만 발생합니다
지연 계산은 일반적으로 직관적으로 보입니다 (계산할 필요가 없는 것은 먼저 계산하지 않음). 하지만 특수한 것은, 타입 관련 장면에는 암묵의 계산이 존재한다는 것입니다 (그다지 직관적이지 않음)
undefined
undefined 는 오류를 일으키는 계산을 나타냅니다:
> undefined
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
undefined, called at <interactive>:12:1 in interactive:Ghci1
지연성을 검사 하는 데 사용할 수 있습니다 (계산이 실제로 실행되었는지 여부). 예를 들어:
> head [1, undefined, 3, undefined, undefined]
1
> let (a, _) = (1, undefined) in a + 1
2
특수하게, 함수 호출 시의 패턴 매칭 자체는 계산이 필요합니다. 매칭 결과가 사용되는지 여부와 관계없이, 예를 들어:
sayHello (_, _) = "hoho"
> sayHello undefined
"*** Exception: Prelude.undefined
CallStack (from HasCallStack):
error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
undefined, called at <interactive>:37:10 in interactive:Ghci17
그리고以下の형식의 것은 계산되지 않습니다:
sayHello _ = "hoho"
> sayHello undefined
"hoho"
양자의 차이는, 전자에 대해서는, Tuple 의 어느 값 컨스트럭터를 사용해야 하는지를 보기 위해 기본적인 계산을 수행할 필요가 있는 반면, 후자는 불필요하다는 것입니다
하지만 이상한 것은, Tuple 에는 명확히 값 컨스트럭터가 1 개만 있다는 것입니다 (「Tuple 의 어느 값 컨스트럭터를 사용해야 하는지를 볼」필요가 없음):
data () = ()
Tuple 의 어느 값 컨스트럭터를 사용해야 하는지를 확인할 필요가 없다는 것은我们知道 있지만, Haskell 은 모릅니다. 왜냐하면, 약속에 따라, data 키워드로 정의된 데이터 타입은 여러 개의 값 컨스트럭터를 가질 수 있으며, 1 개만 선언했어도, 그것을 찾아봐야 알기 때문입니다. 그렇다면, 무엇을 생각했습니까?
newtype 입니다. 값 컨스트럭터가 1 개만 (かつ이 값 컨스트럭터는 파라미터가 1 개만) 이라는 것을 명확히 약속하고 있습니다. 시도해 봅시다:
newtype MyTuple a b = MyTuple {getTuple :: (a, b)}
> sayHello (MyTuple _) = "hh"
> sayHello undefined
"hh"
확실히 그通り로, Haskell 은 충분히 똑똑하여, 여러 개의 값 컨스트럭터가 존재하지 않는 것이 명확한 경우, 무의미한 계산을 수행하지 않습니다
아직 댓글이 없습니다