[haskell] 유형 검사기가 매우 잘못된 유형 대체를 허용하고 있으며 프로그램은 여전히 ​​컴파일됩니다.

내 프로그램에서 문제를 디버깅하는 동안 (동일한 반경을 가진 2 개의 원이 Gloss를 사용하여 다른 크기로 그려집니다 *) 이상한 상황을 발견했습니다. 객체를 처리하는 파일에는 다음과 같은 정의가 있습니다 Player.

type Coord = (Float,Float)
data Obj =  Player  { oPos :: Coord, oDims :: Coord }

Objects.hs를 가져 오는 주 파일에는 다음과 같은 정의가 있습니다.

startPlayer :: Obj
startPlayer = Player (0,0) 10

이것은 플레이어에 대한 필드를 추가하고 변경하고 startPlayer이후 에 업데이트하는 것을 잊었 기 때문에 발생했습니다 Coord. 플레이어는 원이 아닌 개체).

놀라운 점은 두 번째 필드가 잘못된 유형 임에도 불구하고 위의 코드가 컴파일되고 실행된다는 것입니다.

처음에는 다른 버전의 파일이 열려있을 수 있다고 생각했지만 파일의 변경 사항은 컴파일 된 프로그램에 반영되었습니다.

다음으로 나는 startPlayer어떤 이유로 사용되지 않을 것이라고 생각했습니다 . 주석 처리 startPlayer하면 컴파일러 오류가 발생하고, 더 이상하게도 10in을 변경하면 startPlayer적절한 응답이 발생합니다 (의 시작 크기가 변경됨 Player). 다시 말하지만, 잘못된 유형 임에도 불구하고. 데이터 정의를 올바르게 읽고 있는지 확인하기 위해 파일에 오타를 삽입했는데 오류가 발생했습니다. 그래서 올바른 파일을 찾고 있습니다.

위의 2 개 스 니펫을 자신의 파일에 붙여 넣으려고했는데 Playerin 의 두 번째 필드 startPlayer가 올바르지 않다는 예상 오류가 발생했습니다 .

이것이 일어날 수있는 일이 무엇일까요? 이것이 Haskell의 유형 검사기가 방지해야하는 바로 그 것이라고 생각할 것입니다.


* 내 원래 문제에 대한 답은 반경이 같을 것으로 추정되는 두 개의 원이 서로 다른 크기로 그려 지는데 반경 중 하나가 실제로 음수라는 것입니다.



답변

이것이 컴파일 될 수있는 유일한 방법은 Num (Float,Float)인스턴스 가있는 경우입니다. 이것은 표준 라이브러리에서 제공하지 않지만 사용중인 라이브러리 중 하나가 어떤 미친 이유로 추가했을 수 있습니다. ghci에서 프로젝트를로드하고 10 :: (Float,Float)작동하는지 확인한 다음 :i Num인스턴스가 어디에서 오는지 알아 내고 정의한 사람에게 소리를 지르십시오.

부록 : 인스턴스를 끌 수있는 방법이 없습니다. 모듈에서 내 보내지 않는 방법조차 없습니다 . 이것이 가능하다면 코드를 더욱 혼란스럽게 만들 것 입니다. 여기서 유일한 해결책은 그런 인스턴스를 정의하지 않는 것입니다.


답변

Haskell의 유형 검사기가 합리적입니다. 문제는 당신이 사용하고있는 라이브러리의 저자가 뭔가 … 덜 합리적이라는 것입니다.

간단한 대답은 예, 10 :: (Float, Float)인스턴스가있는 경우 완벽하게 유효합니다 Num (Float, Float). 컴파일러 나 언어의 관점에서 “매우 잘못된”것은 없습니다. 숫자 리터럴이하는 일에 대한 우리의 직관과 맞지 않습니다. 당신이 만든 종류의 오류를 잡는 타입 시스템에 익숙하기 때문에 당연히 놀라고 실망했습니다!

Num인스턴스 및 fromInteger문제

컴파일러가 10 :: Coord, 즉 10 :: (Float, Float). 같은 숫자 리터럴 10이 “숫자”유형을 갖는 것으로 유추 될 것이라고 가정하는 것이 합리적 입니다. 상자 중, 숫자 리터럴로 해석 될 수있다 Int, Integer, Float, 또는 Double. 다른 컨텍스트가없는 숫자의 튜플은이 네 가지 유형이 숫자라는 점에서 숫자처럼 보이지 않습니다. 우리는에 대해 말하는 것이 아닙니다 Complex.

다행히도 안타깝게도 Haskell은 매우 유연한 언어입니다. 표준은 같은 정수 리터럴 이 유형 10이있는로 해석되도록 지정합니다 . 따라서 인스턴스가 작성된 모든 유형 으로 추론 할 수 있습니다 . 나는 이것을 다른 대답 에서 조금 더 자세히 설명합니다 .fromInteger 10Num a => a10Num

당신이 당신의 질문을 게시 때, 경험 Haskeller 즉시에 대한 것을 발견 10 :: (Float, Float)인정하는, 같은 인스턴스가 있어야 Num a => Num (a, a)또는 Num (Float, Float). 에 그러한 인스턴스가 없으므로 Prelude다른 곳에서 정의되었을 것입니다. 을 사용 :i Num하여 gloss패키지의 출처를 빠르게 발견했습니다 .

유형 동의어 및 고아 인스턴스

하지만 잠깐만 요. gloss이 예제에서는 어떤 유형도 사용하지 않습니다 . 의 인스턴스가 왜 gloss당신에게 영향을 미쳤습니까? 대답은 두 단계로 나뉩니다.

첫째, 키워드와 함께 도입 된 유형 동의어 type는 새 유형을 생성하지 않습니다 . 모듈에서 작성 Coord은 간단히 (Float, Float). 마찬가지로에서 Graphics.Gloss.Data.Point, Point방법 (Float, Float). 즉, 당신 CoordglossPoint는 문자 그대로 동일합니다.

그래서 gloss메인테이너가 쓰기를 선택 했을 때 instance Num Point where ..., 그들은 또한 당신의 Coord타입을 Num. 즉 동등의 instance Num (Float, Float) where ...instance Num Coord where ....

(기본적으로 Haskell은 유형 동의어가 클래스 인스턴스가되는 것을 허용하지 않습니다. gloss작성자는 인스턴스를 작성하기 위해 한 쌍의 언어 확장 TypeSynonymInstances및 을 활성화해야했습니다 FlexibleInstances.)

둘째, 이것은 고아 인스턴스 , 즉 및 instance C A둘 다 다른 모듈에 정의 된 인스턴스 선언 이기 때문에 놀랍습니다 . 여기서 각 부분이 관여하기 때문에 즉, 교활한입니다 , 그리고 으로부터 온다, 사방 범위에있을 가능성이 높습니다.CANum(,)FloatPrelude

당신의 기대는에 Num정의되어 Prelude있고 튜플과 Float에 정의되어 Prelude있기 때문에이 세 가지가 어떻게 작동하는지에 대한 모든 것이에서 정의됩니다 Prelude. 완전히 다른 모듈을 가져 오면 왜 어떤 것이 변경됩니까? 이상적으로는 그렇지 않지만 고아 인스턴스는 직관을 깨뜨립니다.

(GHC는 고아 인스턴스에 대해 gloss경고합니다. 작성자는 특별히 해당 경고를 무시했습니다. 이로 인해 위험 신호가 발생하고 문서에 최소한 경고가 표시되었을 것입니다.)

클래스 인스턴스는 전역이며 숨길 수 없습니다.

또한, 클래스 인스턴스는 글로벌 : 이적으로 수입되는 모든 모듈에 정의 된 인스턴스 당신의 인스턴스 해상도를 수행 할 때 typechecker에 컨텍스트 및 사용할 수있을 것 모듈. 이렇게하면 전역 추론이 편리해집니다. 왜냐하면 (보통) 같은 클래스 함수 (+)가 주어진 유형에 대해 항상 동일 할 것이라고 가정 할 수 있기 때문 입니다. 그러나 이는 또한 지역적 결정이 전 세계적인 영향을 미친다는 것을 의미하기도합니다. 클래스 인스턴스를 정의하면 모듈 경계 뒤에 마스킹하거나 숨길 방법이없이 다운 스트림 코드의 컨텍스트가 변경됩니다.

당신은 인스턴스를 가져 피하기 위해 수입 목록을 사용할 수 없습니다 . 마찬가지로 정의한 모듈에서 인스턴스를 내보내는 것을 피할 수 없습니다.

이것은 Haskell 언어 디자인에서 문제가 많고 논의가 많은 영역입니다. 이 reddit 스레드 에는 관련 문제에 대한 흥미로운 토론 있습니다. 예를 들어 인스턴스에 대한 가시성 제어를 허용하는 것에 대한 Edward Kmett의 의견을 참조하십시오. “기본적으로 내가 작성한 거의 모든 코드의 정확성을 버립니다.”

(AS 그런데, 이 대답은 증명 , 당신은 할 수 고아 인스턴스를 사용하여 일부 관련 분야의 글로벌 인스턴스 가정을 깰!)

해야 할 일-라이브러리 구현 자

구현하기 전에 두 번 생각하십시오 Num. fromInteger문제를 해결할 수는 없습니다. 정의 fromInteger = error "not implemented"가 더 나아지지는 않습니다 . 사용자의 정수 리터럴이 실수로 인스턴스화중인 유형을 갖는 것으로 추론되는 경우 사용자가 혼란 스럽거나 놀라게 될까요? 제공 (*)하고 (+)그것은 당신이 중요, 특히 경우를 해킹?

Conal Elliott vector-space(종류 유형 *) 또는 Edward Kmett linear( 종류 유형 ) 와 같은 라이브러리에 정의 된 대체 산술 연산자를 사용하는 것을 고려하십시오 * -> *. 이것이 제가하는 경향이 있습니다.

사용 -Wall. 고아 인스턴스를 구현하지 말고 고아 인스턴스 경고를 비활성화하지 마십시오.

또는 linear다른 많은 잘 작동하는 라이브러리 의 리드를 따르고 .OrphanInstances또는로 끝나는 별도의 모듈에서 고아 인스턴스를 제공 .Instances합니다. 그리고 다른 모듈에서 해당 모듈을 가져 오지 마십시오 . 그런 다음 사용자는 원하는 경우 고아를 명시 적으로 가져올 수 있습니다.

고아를 정의하고 있다면 가능하고 적절하다면 업스트림 유지 관리자에게 대신 구현하도록 요청하십시오. 고아 인스턴스 Show a => Show (Identity a)를 추가 할 때까지 자주 작성 했습니다 transformers. 나는 그것에 대한 버그 보고서를 제기했을 수도 있습니다. 기억이 안나요.

해야 할 일 — 도서관 소비자 용

옵션이 많지 않습니다. 도서관 관리자에게 정중하고 건설적으로 연락하십시오. 이 질문을 지적하십시오. 그들은 문제가있는 고아를 쓸 특별한 이유가 있었거나 깨닫지 못할 수도 있습니다.

더 광범위하게 :이 가능성을 인식하십시오. 이것은 진정한 글로벌 효과가있는 Haskell의 몇 안되는 영역 중 하나입니다. 가져 오는 모든 모듈과 해당 모듈이 가져 오는 모든 모듈 고아 인스턴스를 구현하지 않는지 확인해야합니다. 유형 주석은 때때로 문제에 대해 경고 할 수 있으며 물론 :iGHCi에서 확인할 수 있습니다 .

충분히 중요한 경우 동의어 newtype대신 고유 한를 정의하십시오 type. 아무도 그들을 엉망으로 만들지 않을 것이라고 확신 할 수 있습니다.

오픈 소스 라이브러리에서 파생되는 문제가 자주 발생하는 경우 물론 자신 만의 라이브러리 버전을 만들 수 있지만 유지 관리는 금방 골칫거리가 될 수 있습니다.


답변