더 높은 종류의 유형 변수가있는 Functor
Haskell 의 유형 클래스를 고려하십시오 f
.
class Functor f where
fmap :: (a -> b) -> f a -> f b
이 유형 서명이 말하는 것은 fmap이의 유형 매개 변수를 f
from a
으로 변경 b
하지만 f
그대로 유지한다는 것입니다. 따라서 fmap
목록을 통해 사용 하면 목록을 얻을 수 있고 파서를 통해 사용하면 구문 분석기를 얻게되는 식입니다. 그리고 이것들은 정적입니다 , 컴파일 타임 보장입니다.
저는 F #을 모릅니다.하지만 Functor
상속과 제네릭을 사용하여 더 높은 종류의 제네릭이없는 Java 또는 C #과 같은 언어로 추상화 를 표현하려고하면 어떤 일이 발생하는지 살펴 보겠습니다 . 첫 시도:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
이 첫 번째 시도의 문제점은 인터페이스 구현이 를 구현하는 모든 클래스 를 반환 할 수 있다는 것 Functor
입니다. 누군가는 FunnyList<A> implements Functor<A>
그 map
메서드가 다른 종류의 컬렉션을 반환하거나 컬렉션이 아니지만 여전히 Functor
. 또한 map
메서드 를 사용할 때 실제로 예상하는 유형으로 다운 캐스트하지 않는 한 결과에 대해 하위 유형별 메서드를 호출 할 수 없습니다. 따라서 두 가지 문제가 있습니다.
- 유형 시스템은
map
메서드가 항상 Functor
수신자 와 동일한 하위 클래스를 반환 한다는 불변성을 표현할 수 없습니다 .
- 따라서
Functor
의 결과에 대해 비 메서드 를 호출하는 정적으로 형식이 안전한 방식 은 없습니다 map
.
시도 할 수있는 다른 더 복잡한 방법이 있지만 실제로 작동하는 방법은 없습니다. 예를 들어, Functor
결과 유형을 제한 하는 하위 유형을 정의하여 첫 번째 시도를 보강 할 수 있습니다.
interface Collection<A> extends Functor<A> {
Collection<B> map(Function<A, B> f);
}
interface List<A> extends Collection<A> {
List<B> map(Function<A, B> f);
}
interface Set<A> extends Collection<A> {
Set<B> map(Function<A, B> f);
}
interface Parser<A> extends Functor<A> {
Parser<B> map(Function<A, B> f);
}
이는 좁은 인터페이스의 구현자가 메서드 Functor
에서 잘못된 유형을 반환하는 것을 방지하는 데 도움이 map
되지만, Functor
구현할 수있는 구현 수에 제한이 없기 때문에 필요한 더 좁은 인터페이스 수에는 제한이 없습니다.
( 편집 : 그리고 이것은 Functor<B>
결과 유형으로 나타나기 때문에 작동하므로 자식 인터페이스가 범위를 좁힐 수 있습니다. 따라서 AFAIK는 Monad<B>
다음 인터페이스에서 두 가지 용도를 모두 좁힐 수 없습니다 .
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
Haskell에서 상위 유형 변수를 사용하는 경우 이것은입니다 (>>=) :: Monad m => m a -> (a -> m b) -> m b
.)
또 다른 시도는 재귀 제네릭을 사용하여 인터페이스가 하위 유형의 결과 유형을 하위 유형 자체로 제한하도록하는 것입니다. 장난감 예 :
interface Semigroup<T extends Semigroup<T>> {
T append(T arg);
}
class Foo implements Semigroup<Foo> {
Foo append(Foo arg);
}
class Bar implements Semigroup<Bar> {
Semigroup<Bar> append(Semigroup<Bar> arg);
Semigroup<Foo> append(Bar arg);
Semigroup append(Bar arg);
Foo append(Bar arg);
}
그러나 이런 종류의 기술 (당신의 평범한 OOP 개발자에게는 다소 비현실적이며 평범한 기능 개발자에게는 도대체)은 여전히 원하는 Functor
제약을 표현할 수 없습니다 .
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
여기서 문제는이 제한하지 않는 것입니다 FB
같은 가지고 F
같은 FA
당신이 유형을 선언 할 때 - 그럼 List<A> implements Functor<List<A>, A>
의 map
방법은 수 여전히 을 반환을 NotAList<B> implements Functor<NotAList<B>, B>
.
원시 유형 (매개 변수화되지 않은 컨테이너)을 사용하여 Java에서 마지막 시도 :
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
여기에서 F
just List
또는 같은 매개 변수화되지 않은 유형으로 인스턴스화됩니다 Map
. 이것은 FunctorStrategy<List>
a가List
있지만 목록의 요소 유형을 추적하는 데 유형 변수 사용을 포기했습니다.
여기서 문제의 핵심은 Java 및 C #과 같은 언어가 유형 매개 변수가 매개 변수를 갖는 것을 허용하지 않는다는 것입니다. 경우 자바에서 T
형태 변수는, 당신은 쓸 수 T
및 List<T>
하지만, T<String>
. 더 높은 종류의 유형은이 제한을 제거하므로 다음과 같은 것을 가질 수 있습니다 (완전히 고려되지 않음).
interface Functor<F, A> {
<B> F<B> map(Function<A, B> f);
}
class List<A> implements Functor<List, A> {
<B> List<B> map(Function<A, B> f) {
}
}
특히이 비트를 처리합니다.
(나는 생각한다) 대신 myList |> List.map f
또는 myList |> Seq.map f |> Seq.toList
더 높은 종류의 유형을 사용하면 간단하게 작성할 myList |> map f
수 있으며 List
. 훌륭하지만 (정확하다고 가정 할 때) 약간 사소한 것 같나요? (그리고 단순히 함수 오버로딩을 허용하는 것만으로 할 수는 없나요?) 저는 보통로 변환 Seq
한 다음 나중에 원하는대로 변환 할 수 있습니다.
map
이러한 방식으로 함수 의 개념을 일반화하는 많은 언어가 있습니다. 마치 매핑이 시퀀스에 관한 것처럼 모델링함으로써. 이 발언은 그 정신입니다.에서 변환을 지원하는 유형이있는 경우를 Seq
재사용하여 "무료"로지도 작업을 수행 할 수 Seq.map
있습니다.
그러나 Haskell에서는 Functor
클래스가 그것보다 더 일반적입니다. 그것은 시퀀스의 개념에 묶여 있지 않습니다. 액션, 파서 결합 자, 함수 fmap
등과 같이 시퀀스에 대한 좋은 매핑이없는 유형에 대해 구현할 수 있습니다 IO
.
instance Functor IO where
fmap f action =
do x <- action
return (f x)
newtype Function a b = Function (a -> b)
instance Functor (Function a) where
fmap f (Function g) = Function (f . g)
"매핑"의 개념은 실제로 시퀀스와 관련이 없습니다. 펑터 법칙을 이해하는 것이 가장 좋습니다.
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
매우 비공식적으로 :
- 첫 번째 법칙은 identity / noop 함수를 사용한 매핑이 아무것도하지 않는 것과 같다고 말합니다.
- 두 번째 법칙은 두 번 매핑하여 생성 할 수있는 결과를 한 번 매핑하여 생성 할 수도 있다는 것입니다.
이것이 fmap
유형을 보존 하려는 이유 map
입니다. 다른 결과 유형을 생성하는 연산을 받는 즉시 이와 같은 보장을하기가 훨씬 더 어려워지기 때문입니다.