3

형식이 Uuidid 필드가있는 레코드의 형식을 지정하려고합니다. 그래서, 나는 확장 가능한 레코드를 사용하고있다. 유형은 다음과 같습니다.확장 가능 레코드 및 고차 함수

type alias WithId a = { a | id : Uuid } 

지금까지는 그렇게 좋았습니다. 그런 다음 Json.Encoder 즉이 유형의 유형 인 WithId a -> Value의 함수를 만들려고했습니다. 기본 값을 인코딩하는 방법이 필요하므로 형식 a -> Value의 첫 번째 인수를 원합니다. 엘름이 Value에 대한 데이터 유형을 공개하지 않더라도 a이 레코드라는 것을 알고 있으므로 안전하게 JSON 객체로 인코딩된다고 가정 할 수 있습니다.

The argument to function `encoder` is causing a mismatch. 

27|    encoder a 
         ^
Function `encoder` is expecting the argument to be: 

    a 

But it is: 

    WithId a 

Hint: Your type annotation uses type variable `a` which means any type of value 
can flow through. Your code is saying it CANNOT be anything though! Maybe change 
your type annotation to be more specific? Maybe the code has a problem? More at: 
<https://github.com/elm-lang/elm-compiler/blob/0.18.0/hints/type-annotations.md> 

Detected errors in 1 module. 

내가 혼란 스러워요 : 나는 그런 기능을 만들 때

그러나, 나는 컴파일 오류가 발생합니다. 형식이 WithId a 인 경우 a의 모든 입력란을 포함하지 않으므로 WithId aa에서 type alias 구조 지정 유형이 아니어야합니까?

무엇이 누락 되었습니까? WithId a의 유형 별칭이 으로 정의되어 있어도 의 인스턴스로 WithId a을 사용할 수있는 이유는 무엇입니까?

부칙

나는 (". 당신은 그냥 할 수는 없지만, 여기가 일을 할 거 야 무슨"하는 금액) 아래에 답을 표시 한,하지만, 난 여전히 해요 조금은 만족하지 못했다. 나는 type alias이라는 용어의 사용에 대해 혼란스러워합니다. 필자가 이해할 수있는 것은 레코드가 항상 type alias이라는 것입니다. 왜냐하면 그들은 가능한 모든 필드 중에서 필드의 서브 세트를 명시 적으로 지정했기 때문입니다. 그러나 기본 유형은 여전히 ​​통합 레코드 유형 (JS 오브젝트와 유사)이었습니다.

type alias Foo = { bar : Int } 

대신 나에게

type Foo = { bar : Int } 

전 의미 { bar : Int } 어떤 기록이 나에게 의미하는 것 Foo이라고 :

나는 우리가 말하는 이유를 이해하지 못하는 것 같아요 {a|bar:Int}은 모두 aFoo과 동일한 유형입니다. 내가 여기서 뭐라구? 나는 혼란스러워. 나는 내가 기록을 깨우지 않는 것처럼 느낀다.

부칙 2

내 목표는 단지 ​​유형 필드에 .id이 지정하는 것이 아니라 두 가지 유형 가지고 WithId 가지고되지 않습니다 : Foo{ bar:Int }WithId Foo는이 구조를 가지고 어디 FooWithId Foo을 구조 { bar:Int, id:Uuid}. 나는 또한 WithId Baz, WithId Quux 등을 가지고 싶습니다. WithId a에 대해 단일 인코더 및 단일 디코더 기능이 있습니다.

기본 문제는 내가 지속되고 비 지속되는 데이터입니다. 단 하나의 데이터가 유지되면 ID가 있다는 점만 다릅니다.그리고 특정 유형의 레코드가 지속되도록하는 유형 수준 보증을 원하므로 Maybe이 작동하지 않습니다. 우리가 WithId a의 인스턴스에 대해 알고

+0

레코드의 경우, 나는 문제를 해결할 수있는 확장 가능한 레코드를 포기하고'(Uuid, a) '튜플 주위의 형식 별칭을 사용했습니다. 내가 원했던만큼 훌륭한 API는 아니지만 작동합니다. 나는 왜 내가하고 싶은 일을 할 수 없는지 혼란 스럽다. –

+1

다음은 Elm 커뮤니티의 오래되고 반복되는 대화입니다 : https://github.com/elm-lang/elm-compiler/issues/1308#issuecomment-197170407 –

답변

5

당신이 WithId a 값에 대해 알고 당신이 때문에 그것으로 할 수 있어야 어떤 일에 대해 당신의 추론 방법 (일반 a로 취급)는 논리적으로 소리이지만, 확장 레코드 느릅 나무의 지원은하지 않습니다 그것을 허락하는 방식으로 타이프 시스템과 놀아 라. 실제로, 느릅 나무의 확장 레코드 유형은 특히 전체 기록에 대한 액세스를 허용하지을 설계되었습니다. 작은 작업 기능을 작성하는

type alias WithId a = 
    { a | id : Uuid } 

isValidId : WithId a -> Bool 
isValidId withId = 
    -- can only access .id 

updateId : WithId a -> WithId a 
updateId withId = 
    { withId | 
     id = -- can only access .id 
    } 

을 사용 : 함수는 레코드의 특정 필드에 액세스하는 것을 선언하는 것입니다 위해 설계되었으며, 컴파일러는 그 제약 조건을 적용하는 확장 가능한 어떤 기록

예를 들어, 대형 프로그램 모델의 슬라이스는 주어진 함수가 수행 할 수있는 것에 대한 강력한 보장을 제공합니다. 귀하의 updateUser 기능은, 예를 들어, 오히려 모델에 아마 필드의 수십에 액세스 할 수없는 통치를하는 것보다, 모델의 user 필드에 액세스하는 데에만 제한 될 수 있습니다. 나중에 예기치 않은 모델 변경 사항을 디버그하려고 할 때 이러한 제약 조건은 많은 기능을 해당 변경 사항의 원인으로 신속하게 배제하는 데 도움이됩니다.

위의 구체적인 예를 보려면 Richard Feldman의 elm-europe 2017 talk(확장 가능한 레코드를 표시하는 부분은 해당 비디오의 23:10 참조)을 권합니다.

내가 처음으로 확장 가능한 레코드에 대해 알았을 때 나는 같은 흥분과 혼란스러운 실망을 겪었지만 뒤늦은 생각으로는 내 깊이 뿌리깊은 객체 지향 프로그래밍의 본능에서 나온 것이라고 생각합니다. 나는 "확장 성"을 보았고 나의 두뇌는 곧바로 "상속"으로 뛰어 올랐다. 그러나 Elm은 유형 상속을 지원할 수있는 방식으로 확장 가능한 레코드를 구현하지 않습니다.

확장 레코드의 진정한 의도 된 목적은, 상술 한 실시 예에 도시 된 바와 같이, 많은 레코드 타입의 서브 세트에 기능의 액세스를 제한하는 것이다.

각각의 데이터 유형에 대해 Encoder으로 시작하는 것이 좋으며 ID 인코더 구현을 다시 사용하려면 각 인코더에서 idEncoder 공유 함수를 호출하십시오 .

+0

답변을 주셔서 감사합니다. 답변으로 표시했는데 "타입 별명"이라는 용어의 사용에 혼란 스럽습니다. 필자는 레코드가 가능한 모든 필드 중에서 필드의 하위 집합을 명시 적으로 지정했기 때문에 레코드가 항상 "형식 별칭"이었지만 기본 형식은 여전히 ​​통합 레코드 형식 (JS 개체와 유사 함)이었습니다. 나는 우리가 말할 이유를 이해하지 못하는 것 같아요 : 유형 별칭 푸 = {바 : 지능} 대신 유형 푸 = {바 : 지능} 전자는 나에게 의미가 {바가 아무것도 : Int}는 Foo입니다. 내가 여기서 뭐라구? 나는 혼란스러워. –

+0

그 주석은 정말 길어서 위 질문에 대한 부록을 추가했습니다. –

2

있는 유일한 방법은이 .id 필드가입니다. 우리가 WithId a를 인코딩하는 경우 우리가 할 수있는 최선은 .id 값을 인코딩입니다.

type alias Uuid = 
    String 

encodeUuid : Uuid -> Value 
encodeUuid = 
    Encode.string 

type alias WithId a = 
    { a | id : Uuid } 

encode : WithId a -> Value 
encode withId = 
    encodeUuid withId.id 
+1

진술은 사실이 아닙니다. 나는 더 많이 안다. 또한 WithId a는 언 바운드 변수이므로 자체적으로 흥미롭지 않은'a '의 모든 필드를가집니다. 그러나'(a -> Value)'및'(a -> Value) WithId a -> Value)'를 호출하면 함수에'a'를 전달하고 'Value'를 얻을 수 있다는 것을 알았습니다. 'WithId a'에는'a'의 모든 필드가 있으므로'a'로 처리 할 수있을 것으로 기대했습니다. 그것은 엘름에서의 잘못된 가정 일뿐입니다. 그것은 오래된 포인트처럼 보입니다. https://github.com/elm-lang/elm-compiler/issues/1308#issuecomment-197170407 –

1

은 부록을 읽고 질문을 다시 읽고, 난 당신이 실제로 당신이 느릅 나무에서 확장 레코드 원하는 것을 얻을 수 있다고 생각합니다.

module Main exposing (main) 

import Html exposing (Html, pre, text) 
import Json.Encode as Json exposing (Value, int, object, string) 


main : Html msg 
main = 
    let 
     json = 
      Json.encode 2 <| 
       encoder recordEncoder record 
    in 
    pre [] [ text json ] 


type alias WithId a = 
    { a | id : Int } 


type alias Record = 
    WithId 
     { fieldA : String 
     } 


record : Record 
record = 
    { id = 123 
    , fieldA = "A" 
    } 


encoder : (WithId a -> Value) -> WithId a -> Value 
encoder recordEncoder value = 
    object 
     [ ("id", int value.id) 
     , ("value", recordEncoder value) 
     ] 


recordEncoder : Record -> Value 
recordEncoder record = 
    object 
     [ ("fieldA", string record.fieldA) ] 
first answer 당으로

View on Ellie - The Elm Live Editor

나는 아직도 경우 잘 작동하지 않습니다 확신 해요 : 여기

당신이 계신 수행하고 잘 컴파일하는 작은 프로그램입니다 이 패턴을 전체 프로그램으로 확장 할 수 있지만, Elm에 대한 좋은 점은 이런 식으로 시도해 볼 수 있다는 것입니다. 컴파일러가 작동하지 않으면 리팩터링에 도움이됩니다!

... 그리고 당신이 정말로 지속 및 프로그램 비 지속 기록을 모두 처리 할 수 ​​있어야합니다, 당신은 당신의 유형 별칭에 조금 반복으로 작업을 수행 할 수 있습니다

module Main exposing (main) 

import Html exposing (Html, pre, text) 
import Json.Encode as Json exposing (Value, int, object, string) 


main : Html msg 
main = 
    let 
     json = 
      Json.encode 2 <| 
       encoder recordEncoder record 
    in 
    pre [] [ text json ] 


type alias WithId a = 
    { a 
     | id : Int 
    } 


type alias Record a = 
    { a 
     | fieldA : String 
    } 


type alias PersistedRecord = 
    { id : Int 
    , fieldA : String 
    } 


record : PersistedRecord 
record = 
    { id = 123 
    , fieldA = "A" 
    } 


encoder : (WithId a -> Value) -> WithId a -> Value 
encoder recordEncoder value = 
    object 
     [ ("id", int value.id) 
     , ("value", recordEncoder value) 
     ] 


recordEncoder : Record a -> Value 
recordEncoder record = 
    object 
     [ ("fieldA", string record.fieldA) ] 

View on Ellie - The Elm Live Editor

직접 부록의이 부분에 대답하려면 :

I guess I don't understand why we say:

type alias Foo = { bar : Int } 

instead of

type Foo = { bar : Int } 

The former implies to me that any record with { bar : Int } is a Foo, which would imply to me that {a|bar:Int} is both of the same type as a and a Foo . Where am I wrong here? I'm confused. I feel like I'm not grokking records.

{ a | bar:Int }Foo 아니라, 때문에 Foo 유형 별칭은 고정 된 필드 세트를 지정합니다. Foobar 필드 을 포함하고 다른 필드는이 아닙니다. "이 필드가있는 레코드 및 가능하면 다른 사람"이라고 말하면 확장 가능 레코드 구문 ({ a | …fields… })을 사용해야합니다.

그래서 내가 잘못 생각하는 부분은 모든 레코드 유형이 확장 가능하다고 가정하는 것입니다. 그들은 그렇지 않습니다. 함수가 고정 된 필드 집합을 사용하여 레코드를 가져 오거나 반환한다고 말하면 해당 값에는 다른 필드가 포함되지 않습니다. 즉, 컴파일러는이를 보장합니다.

+0

이 연습의 요점은 모든 모델에서 모든 필드를 두 번 반복하는 것을 피하기 위해 노력하고 있다는 것입니다. 추가 idid 필드를 한 번만 추가하면됩니다.이 작업을 수행 할 수 있다고 가정합니다. 'RecordWithId = WithId Record', 그래서 적어도 유형 안전성이 있지만 의미있는 디코더/엔코더 재사용은 없습니다. 최소한은 시작입니다. –

+1

내 혼란은 레코드가 '형식 별칭'이고 '형식'이 아닌 이유 인 것 같습니다. 명시 적으로 아무 것도 없다고 말하면, 그 별칭은 무엇입니까? 그것은 그것이 구조적으로 타입 화 되었기 때문입니다. 그러나 필드의 수, 이름 및 유형에 정확히 일치하는 경우에만? –

+1

@RobertFischer 네, 맞습니다. 저는 개인적으로'type'을 완전히 새로운 값을 컴파일러에 도입하는 것으로 생각합니다. 반면에'type alias'는 컴파일러에게 이름이 없어도 리터럴 값을 만들 수있는 유형에 편리한 이름을 제공합니다. 나는 그것이 얼마나 명확하고 유용한지를 잘 모릅니다. –