저는 Haskell에 능숙하지 않으므로 이것은 매우 쉬운 질문 일 수 있습니다.
Rank2Types 는 어떤 언어 제한을 해결합니까? Haskell의 함수가 이미 다형성 인수를 지원하지 않습니까?
저는 Haskell에 능숙하지 않으므로 이것은 매우 쉬운 질문 일 수 있습니다.
Rank2Types 는 어떤 언어 제한을 해결합니까? Haskell의 함수가 이미 다형성 인수를 지원하지 않습니까?
답변:
Haskell의 함수가 이미 다형성 인수를 지원하지 않습니까?
이 확장 기능없이 다른 유형의 인수를 사용하는 함수를 작성할 수는 있지만 동일한 호출에서 인수를 다른 유형으로 사용하는 함수를 작성할 수는 없습니다.
예를 들어 다음 함수는 g
의 정의에서 다른 인수 유형과 함께 사용 되므로이 확장없이 입력 할 수 없습니다 f
.
f g = g 1 + g "lala"
다형성 함수를 다른 함수의 인수로 전달하는 것은 완벽하게 가능합니다. 그래서 같은 map id ["a","b","c"]
것은 완벽하게 합법적입니다. 그러나 함수는 단형으로 만 사용할 수 있습니다. 예제 map
에서는 id
유형이있는 것처럼 사용 합니다 String -> String
. 물론 id
. 대신 주어진 유형의 단순한 단형 함수를 전달할 수도 있습니다 . rank2types가 없으면 함수가 인수가 다형성 함수 여야하므로 다형성 함수로 사용할 방법이 없습니다.
f' g x y = g x + g y
. 추론 된 순위 1 유형은 forall a r. Num r => (a -> r) -> a -> a -> r
입니다. forall a
함수 화살표 밖에 있으므로 호출자는 먼저 유형을 선택해야합니다 a
. 그들이 선택하는 경우 Int
, 우리가 얻을 f' :: forall r. Num r => (Int -> r) -> Int -> Int -> r
, 그리고 지금 우리가 해결 한 g
이 걸릴 수 있도록 인수를 Int
하지만 String
. 활성화하면 type으로 RankNTypes
주석 f'
을 달 수 있습니다 forall b c r. Num r => (forall a. a -> r) -> b -> c -> r
. 그래도 사용할 수 없습니다. 무엇 g
일까요?
시스템 F를 직접 공부하지 않으면 상위 다형성을 이해하기 어렵습니다. 하스켈은 단순성을 위해 세부 사항을 숨길 수 있도록 설계 되었기 때문입니다.
그러나 기본적으로 대략적인 생각은 다형성 유형이 a -> b
Haskell에서하는 것과 같은 형태가 아니라는 것입니다. 실제로는 다음과 같이 항상 명시 적 수량자를 사용합니다.
id :: ∀a.a → a
id = Λt.λx:t.x
"∀"기호를 모르면 "for all"로 읽습니다. ∀x.dog(x)
"모든 x에 대해 x는 개"라는 뜻입니다. "Λ"는 대문자 람다로, 유형 매개 변수를 추상화하는 데 사용됩니다. 두 번째 줄이 말하는 것은 id는 type을 취한 t
다음 해당 유형에 의해 매개 변수화 된 함수를 반환하는 함수라는 것입니다.
시스템 F에서는 이와 같은 함수 id
를 값에 바로 적용 할 수 없습니다 . 먼저 값에 적용하는 λ- 함수를 얻으려면 Λ- 함수를 유형에 적용해야합니다. 예를 들면 다음과 같습니다.
(Λt.λx:t.x) Int 5 = (λx:Int.x) 5
= 5
표준 Haskell (즉, Haskell 98 및 2010)은 이러한 유형 수량 자, 대문자 람다 및 유형 응용 프로그램을 사용하지 않음으로써이를 단순화합니다. 그러나 GHC는 컴파일을 위해 프로그램을 분석 할 때이를 적용합니다. (이것은 런타임 오버 헤드가없는 모든 컴파일 타임 항목입니다.)
그러나 Haskell의 자동 처리는 "∀"이 함수 ( "→") 유형의 왼쪽 분기에 나타나지 않는다고 가정 함을 의미합니다. Rank2Types
및 RankNTypes
그 제한을 해제하고 삽입하는 위치에 대한 하스켈의 기본 규칙을 재정의 할 수 있습니다 forall
.
왜 이렇게 하시겠습니까? 완전하고 제한되지 않은 System F는 정말 강력하고 멋진 일을 많이 할 수 있기 때문입니다. 예를 들어, 유형 숨김 및 모듈성은 상위 유형을 사용하여 구현할 수 있습니다. 예를 들어 다음 랭크 -1 유형의 평범한 이전 기능을 예로 들어 보겠습니다 (장면을 설정하기 위해).
f :: ∀r.∀a.((a → r) → a → r) → r
를 사용하려면 f
호출자가 먼저 r
및 에 사용할 유형을 a
선택한 다음 결과 유형의 인수를 제공해야합니다. 그래서 당신은 선택할 수 r = Int
및 a = String
:
f Int String :: ((String → Int) → String → Int) → Int
그러나 이제 다음과 같은 상위 유형과 비교하십시오.
f' :: ∀r.(∀a.(a → r) → a → r) → r
이 유형의 기능은 어떻게 작동합니까? 글쎄, 그것을 사용하려면 먼저 사용할 유형을 지정하십시오 r
. 우리가 선택한다고 해보자 Int
:
f' Int :: (∀a.(a → Int) → a → Int) → Int
하지만 지금은이 ∀a
입니다 내부 는 어떤 유형에 사용할 선택할 수 있도록 기능 화살표 a
; f' Int
적절한 유형의 Λ- 기능에 적용해야합니다 . 이것은 구현이 의 호출자가 아니라 f'
사용할 유형을 선택해야a
f'
함을 의미합니다 . 상위 유형이 없으면 반대로 호출자는 항상 유형을 선택합니다.
이것은 무엇에 유용합니까? 글쎄요, 실제로 많은 것들에 대해,하지만 한 가지 아이디어는 이것을 사용하여 객체 지향 프로그래밍과 같은 것을 모델링 할 수 있다는 것입니다. 여기서 "객체"는 숨겨진 데이터에 대해 작동하는 몇 가지 방법과 함께 숨겨진 데이터를 묶습니다. 예를 들어, 하나는 an을 반환 Int
하고 다른 하나 는 a를 반환하는 두 개의 메서드가있는 객체 String
를 다음 유형으로 구현할 수 있습니다.
myObject :: ∀r.(∀a.(a → Int, a -> String) → a → r) → r
어떻게 작동합니까? 개체는 숨겨진 유형의 내부 데이터가있는 함수로 구현됩니다 a
. 객체를 실제로 사용하기 위해 클라이언트는 객체가 두 메서드로 호출 할 "콜백"함수를 전달합니다. 예를 들면 :
myObject String (Λa. λ(length, name):(a → Int, a → String). λobjData:a. name objData)
여기서 우리는 기본적으로 객체의 두 번째 메소드를 호출합니다 . 유형은 a → String
unknown에 대한 것 a
입니다. myObject
의 고객 에게는 알려지지 않았습니다 . 그러나 이러한 클라이언트는 서명을 통해 두 가지 기능 중 하나를 적용 할 수 Int
있고 String
.
실제 Haskell 예제를 위해 아래는 제가 스스로 가르쳤을 때 작성한 코드입니다 RankNTypes
. 이것은 ShowBox
숨겨진 유형의 값을 Show
클래스 인스턴스 와 함께 묶는 라는 유형을 구현 합니다. 맨 아래의 예에서 ShowBox
첫 번째 요소는 숫자로, 두 번째 요소는 문자열로 만든 목록을 만듭니다 . 상위 유형을 사용하여 유형이 숨겨 지므로 유형 검사를 위반하지 않습니다.
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ImpredicativeTypes #-}
type ShowBox = forall b. (forall a. Show a => a -> b) -> b
mkShowBox :: Show a => a -> ShowBox
mkShowBox x = \k -> k x
-- | This is the key function for using a 'ShowBox'. You pass in
-- a function @k@ that will be applied to the contents of the
-- ShowBox. But you don't pick the type of @k@'s argument--the
-- ShowBox does. However, it's restricted to picking a type that
-- implements @Show@, so you know that whatever type it picks, you
-- can use the 'show' function.
runShowBox :: forall b. (forall a. Show a => a -> b) -> ShowBox -> b
-- Expanded type:
--
-- runShowBox
-- :: forall b. (forall a. Show a => a -> b)
-- -> (forall b. (forall a. Show a => a -> b) -> b)
-- -> b
--
runShowBox k box = box k
example :: [ShowBox]
-- example :: [ShowBox] expands to this:
--
-- example :: [forall b. (forall a. Show a => a -> b) -> b]
--
-- Without the annotation the compiler infers the following, which
-- breaks in the definition of 'result' below:
--
-- example :: forall b. [(forall a. Show a => a -> b) -> b]
--
example = [mkShowBox 5, mkShowBox "foo"]
result :: [String]
result = map (runShowBox show) example
추신 : ExistentialTypes
GHC가 어떻게 사용 하는지 궁금해하는이를 읽는 사람에게는 forall
그 이유가이면에서 이런 종류의 기술을 사용하기 때문이라고 생각합니다.
exists
키워드 가있는 경우 실존 유형을 (예)로 정의 할 수 있습니다. data Any = Any (exists a. a)
여기서 Any :: (exists a. a) -> Any
. ∀xP (x) → Q ≡ (∃xP (x)) → Q를 사용 Any
하면 유형도 가질 수 forall a. a -> Any
있으며 forall
키워드의 출처입니다. 나는 GHC에 의해 구현 된 존재 유형은 필요한 모든 유형 클래스 사전을 가지고있는 평범한 데이터 유형일 뿐이라고 믿습니다 (이것을 백업 할 참조를 찾을 수 없습니다, 죄송합니다).
data ApplyBox r = forall a. ApplyBox (a -> r) a
; 경우에 당신이 패턴 일치 ApplyBox f x
, 당신이 얻을 f :: h -> r
하고 x :: h
는 "숨겨진"제한된 유형 h
. 내가 올바르게 이해한다면 typeclass 사전 케이스는 다음과 같이 data ShowBox = forall a. Show a => ShowBox a
번역됩니다 data ShowBox' = forall a. ShowBox' (ShowDict' a) a
. instance Show ShowBox' where show (ShowBox' dict val) = show' dict val
; show' :: ShowDict a -> a -> String
.
Luis Casillas의 답변 은 랭크 2 유형이 무엇을 의미하는지에 대한 많은 정보를 제공하지만 그가 다루지 않은 한 가지 요점 만 확장하겠습니다. 인수를 다형성으로 요구한다고해서 여러 유형과 함께 사용할 수있는 것은 아닙니다. 또한 해당 함수가 인수로 수행 할 수있는 작업과 결과를 생성하는 방법을 제한합니다. 즉, 호출자에게 유연성이 떨어 집니다. 왜 그렇게 하시겠습니까? 간단한 예부터 시작하겠습니다.
데이터 유형이 있다고 가정합니다.
data Country = BigEnemy | MediumEnemy | PunyEnemy | TradePartner | Ally | BestAlly
함수를 작성하고 싶습니다.
f g = launchMissilesAt $ g [BigEnemy, MediumEnemy, PunyEnemy]
주어진 목록의 요소 중 하나를 선택 IO
하고 해당 표적에서 미사일을 발사 하는 동작을 반환하는 함수를 사용합니다 . f
간단한 유형을 제공 할 수 있습니다 .
f :: ([Country] -> Country) -> IO ()
문제는 우연히 실행할 수 있다는 것입니다.
f (\_ -> BestAlly)
그러면 우리는 큰 곤경에 처할 것입니다! 주기 f
순위 1 다형성 유형을
f :: ([a] -> a) -> IO ()
a
우리가를 호출 할 때 유형을 선택하고 f
이를 전문화하고 다시 Country
악성 코드를 사용 하기 때문에 전혀 도움이되지 않습니다 \_ -> BestAlly
. 해결책은 등급 2 유형을 사용하는 것입니다.
f :: (forall a . [a] -> a) -> IO ()
이제 우리가 전달하는 함수는 다형성이어야하므로 \_ -> BestAlly
유형 검사는하지 않습니다! 실제로 주어진 목록에없는 요소를 반환하는 함수 는 유형 검사를하지 않습니다 (무한 루프에 들어가거나 오류를 생성하여 반환하지 않는 일부 함수는 그렇게 할 수 있지만).
물론 위의 내용은 고안되었지만이 기술의 변형은 ST
모나드를 안전하게 만드는 핵심 입니다.
상위 유형은 다른 답변이 만든 것처럼 이국적이지 않습니다. 믿거 나 말거나 많은 객체 지향 언어 (Java 및 C # 포함!)가 이러한 기능을 제공합니다. (물론, 그 커뮤니티의 아무도 "상위 유형"이라는 무서운 이름으로 그들을 알지 못합니다.)
제가 드릴 예제는 Visitor 패턴의 교과서 구현으로, 저는 일상 업무에서 항상 사용 합니다 . 이 답변은 방문자 패턴을 소개하기위한 것이 아닙니다. 그 지식은 다른 곳에서 쉽게 구할 수 있습니다 .
이 상상의 HR 응용 프로그램에서 우리는 정규직 또는 임시 계약직 일 수있는 직원을 대상으로 운영하고자합니다. 방문자 패턴의 선호하는 변형 (실제로와 관련된 것 RankNTypes
)은 방문자의 반환 유형을 매개 변수화합니다.
interface IEmployeeVisitor<T>
{
T Visit(PermanentEmployee e);
T Visit(Contractor c);
}
class XmlVisitor : IEmployeeVisitor<string> { /* ... */ }
class PaymentCalculator : IEmployeeVisitor<int> { /* ... */ }
요점은 다른 반환 유형을 가진 많은 방문자가 모두 동일한 데이터에서 작업 할 수 있다는 것입니다. 이것은 IEmployee
무엇을 T
해야하는지에 대한 의견을 표현 해서는 안된다는 것을 의미 합니다 .
interface IEmployee
{
T Accept<T>(IEmployeeVisitor<T> v);
}
class PermanentEmployee : IEmployee
{
// ...
public T Accept<T>(IEmployeeVisitor<T> v)
{
return v.Visit(this);
}
}
class Contractor : IEmployee
{
// ...
public T Accept<T>(IEmployeeVisitor<T> v)
{
return v.Visit(this);
}
}
유형에주의를 기울이고 싶습니다. 즉 관찰 IEmployeeVisitor
, 보편적으로의 반환 유형을 정량화하는 반면 IEmployee
자사의 내부 정량화를 Accept
방법 - 더 높은 순위에, 말을하는 것입니다. C #에서 Haskell로 어설프게 번역 :
data IEmployeeVisitor r = IEmployeeVisitor {
visitPermanent :: PermanentEmployee -> r,
visitContractor :: Contractor -> r
}
newtype IEmployee = IEmployee {
accept :: forall r. IEmployeeVisitor r -> r
}
그래서 거기에 있습니다. 제네릭 메서드를 포함하는 형식을 작성할 때 더 높은 순위 형식이 C #에 표시됩니다.
객체 지향 언어에 익숙한 사람들에게 상위 함수는 단순히 다른 일반 함수를 인수로 기대하는 일반 함수입니다.
예를 들어 TypeScript에서 다음과 같이 작성할 수 있습니다.
type WithId<T> = T & { id: number }
type Identifier = <T>(obj: T) => WithId<T>
type Identify = <TObj>(obj: TObj, f: Identifier) => WithId<TObj>
제네릭 함수 유형이 유형의 제네릭 함수를 어떻게 Identify
요구 하는지 보십니까 Identifier
? 이것은 Identify
더 높은 순위의 기능을 만듭니다.
Accept
랭크 -1 다형성 유형이 있지만 IEmployee
, 그 자체가 랭크 -2 인의 방법입니다 . 누군가 나에게을 주면 IEmployee
그것을 열어서 Accept
어떤 유형에서든 그 방법을 사용할 수 있습니다 .
Visitee
당신이 소개 하는 수업을 통해 순위 2 입니다. 함수 f :: Visitee e => T e
는 기본적으로 (클래스 항목이 desugared되면)입니다 f :: (forall r. e -> Visitor e r -> r) -> T e
. Haskell 2010을 사용하면 이와 같은 클래스를 사용하여 제한된 랭크 2 다형성에서 벗어날 수 있습니다.
forall
내 예 에서는 떠날 수 없습니다 . 나는 참고 문헌이 없지만 "Scrap Your Type Classes" 에서 뭔가를 찾을 수 있습니다 . 더 높은 순위의 다형성은 실제로 유형 검사 문제를 일으킬 수 있지만 클래스 시스템에 내재 된 제한된 정렬은 괜찮습니다.