ความแตกต่างใน Elm ระหว่าง type และ type alias?


93

ในเอล์ม, ฉันไม่สามารถคิดออกเมื่อเป็นคำหลักที่เหมาะสมกับtype type aliasเอกสารประกอบดูเหมือนจะไม่มีคำอธิบายเกี่ยวกับเรื่องนี้และฉันไม่พบเอกสารในบันทึกประจำรุ่น เอกสารนี้อยู่ที่ไหน

คำตอบ:


136

ฉันคิดอย่างไร:

type ใช้สำหรับกำหนดประเภทสหภาพใหม่:

type Thing = Something | SomethingElse

ก่อนหน้านี้คำจำกัดความSomethingและSomethingElseไม่ได้มีความหมายอะไรเลย ตอนนี้ทั้งสองประเภทThingซึ่งเราเพิ่งกำหนดไว้

type alias ใช้สำหรับตั้งชื่อให้กับประเภทอื่นที่มีอยู่แล้ว:

type alias Location = { lat:Int, long:Int }

{ lat = 5, long = 10 }มีประเภท{ lat:Int, long:Int }ซึ่งเป็นประเภทที่ถูกต้องอยู่แล้ว แต่ตอนนี้เราสามารถพูดได้ว่ามีประเภทLocationเพราะเป็นนามแฝงสำหรับประเภทเดียวกัน

เป็นที่น่าสังเกตว่าต่อไปนี้จะรวบรวมและแสดงผล"thing"ได้ดี แม้ว่าเราจะระบุthingเป็นStringและaliasedStringIdentityใช้เวลาAliasedStringที่เราจะไม่ได้รับข้อผิดพลาดที่ไม่ตรงกันระหว่างประเภทString/ AliasedString:

import Graphics.Element exposing (show)

type alias AliasedString = String

aliasedStringIdentity: AliasedString -> AliasedString
aliasedStringIdentity s = s

thing : String
thing = "thing"

main =
  show <| aliasedStringIdentity thing

ไม่แน่ใจเกี่ยวกับจุดของย่อหน้าสุดท้ายของคุณ คุณพยายามบอกว่าพวกเขายังคงเป็นคนประเภทเดียวกันไม่ว่าคุณจะใช้นามแฝงว่าอย่างไร?
ZHANG Cheng

8
ใช่เพียงแค่ชี้ให้เห็นว่าคอมไพเลอร์พิจารณาว่าประเภทนามแฝงนั้นเหมือนกับต้นฉบับ
robertjlooby

ดังนั้นเมื่อคุณใช้{}ไวยากรณ์ของระเบียนคุณกำลังกำหนดประเภทใหม่หรือไม่?

3
{ lat:Int, long:Int }ไม่ได้กำหนดประเภทใหม่ นั่นเป็นประเภทที่ถูกต้องอยู่แล้ว type alias Location = { lat:Int, long:Int }ยังไม่ได้กำหนดประเภทใหม่ แต่จะให้ชื่ออื่น (อาจสื่อความหมายได้มากกว่า) เป็นประเภทที่ถูกต้องอยู่แล้ว type Location = Geo { lat:Int, long:Int }จะกำหนดประเภทใหม่ ( Location)
robertjlooby

1
เมื่อใดควรใช้ type vs. type alias? ข้อเสียของการใช้ประเภทเสมออยู่ที่ไหน?
Richard Haven

8

aliasที่สำคัญคือคำว่า ในระหว่างการเขียนโปรแกรมเมื่อคุณต้องการจัดกลุ่มสิ่งที่อยู่ด้วยกันคุณจะใส่มันลงในบันทึกเช่นในกรณีของจุด

{ x = 5, y = 4 }  

หรือบันทึกของนักเรียน

{ name = "Billy Bob", grade = 10, classof = 1998 }

ตอนนี้หากคุณต้องการส่งต่อบันทึกเหล่านี้คุณจะต้องสะกดทั้งประเภทเช่น:

add : { x:Int, y:Int } -> { x:Int, y:Int } -> { x:Int, y:Int }
add a b =
  { a.x + b.x, a.y + b.y }

หากคุณสามารถใช้นามแฝงว่าลายเซ็นจะเขียนง่ายกว่ามาก!

type alias Point = { x:Int, y:Int }
add : Point -> Point -> Point
add a b =
  { a.x + b.x, a.y + b.y }

ดังนั้นนามแฝงจึงเป็นชวเลขสำหรับอย่างอื่น นี่คือชวเลขสำหรับประเภทบันทึก คุณอาจคิดว่าเป็นการตั้งชื่อให้กับประเภทบันทึกที่คุณจะใช้บ่อยๆ นั่นเป็นเหตุผลที่เรียกว่านามแฝง - เป็นอีกชื่อหนึ่งของประเภทบันทึกเปล่าที่แสดง{ x:Int, y:Int }

ในขณะที่typeแก้ปัญหาที่แตกต่างกัน หากคุณมาจาก OOP นั่นคือปัญหาที่คุณแก้ด้วยการสืบทอดการทำงานมากเกินไปของตัวดำเนินการและอื่น ๆ - บางครั้งคุณต้องการถือว่าข้อมูลเป็นเรื่องธรรมดาและบางครั้งคุณก็ต้องการที่จะปฏิบัติเหมือนสิ่งที่เฉพาะเจาะจง

สิ่งที่พบเห็นได้ทั่วไปคือเมื่อส่งข้อความไปทั่วเช่นระบบไปรษณีย์ เมื่อคุณส่งจดหมายคุณต้องการให้ระบบไปรษณีย์ถือว่าข้อความทั้งหมดเป็นสิ่งเดียวกันดังนั้นคุณต้องออกแบบระบบไปรษณีย์เพียงครั้งเดียว และนอกจากนี้งานกำหนดเส้นทางข้อความควรเป็นอิสระจากข้อความที่อยู่ภายใน เฉพาะเมื่อจดหมายถึงปลายทางคุณสนใจว่าข้อความนั้นคืออะไร

ในทำนองเดียวกันเราอาจกำหนดtypeว่าเป็นการรวมกันของข้อความประเภทต่างๆทั้งหมดที่อาจเกิดขึ้น สมมติว่าเรากำลังใช้ระบบส่งข้อความระหว่างนักศึกษากับผู้ปกครอง ดังนั้นจึงมีเพียงสองข้อความที่เด็กวิทยาลัยสามารถส่ง: 'ฉันต้องการเงินค่าเบียร์' และ 'ฉันต้องการกางเกงใน'

type MessageHome = NeedBeerMoney | NeedUnderpants

ตอนนี้เมื่อเราออกแบบระบบการกำหนดเส้นทางประเภทของฟังก์ชันของเราสามารถส่งผ่านไปMessageHomeมาได้แทนที่จะกังวลเกี่ยวกับข้อความประเภทต่างๆทั้งหมดที่อาจเป็นได้ ระบบเส้นทางไม่สนใจ เพียงแค่ต้องรู้ว่ามันเป็นไฟล์MessageHome. ก็ต่อเมื่อข้อความไปถึงปลายทางนั่นคือบ้านของผู้ปกครองเท่านั้นที่คุณต้องหาว่ามันคืออะไร

case message of
  NeedBeerMoney ->
    sayNo()
  NeedUnderpants ->
    sendUnderpants(3)

หากคุณรู้จักสถาปัตยกรรม Elm ฟังก์ชันการอัปเดตเป็นคำสั่งกรณีขนาดใหญ่เนื่องจากเป็นปลายทางของการกำหนดเส้นทางข้อความและด้วยเหตุนี้จึงถูกประมวลผล และเราใช้ประเภทยูเนี่ยนเพื่อให้มีประเภทเดียวในการจัดการเมื่อส่งข้อความไปรอบ ๆ แต่สามารถใช้คำสั่งกรณีเพื่อล้อเลียนว่าข้อความนั้นคืออะไรเพื่อให้เราจัดการกับมันได้


5

ให้ฉันเติมเต็มคำตอบก่อนหน้านี้โดยเน้นไปที่กรณีการใช้งานและให้บริบทเล็กน้อยเกี่ยวกับฟังก์ชันตัวสร้างและโมดูล



ประเพณีของ type alias

  1. สร้างนามแฝงและฟังก์ชันตัวสร้างสำหรับเร็กคอร์ด
    นี่คือกรณีการใช้งานที่พบบ่อยที่สุด: คุณสามารถกำหนดชื่อทางเลือกและฟังก์ชันตัวสร้างสำหรับรูปแบบระเบียนเฉพาะ

    type alias Person =
        { name : String
        , age : Int
        }
    

    การกำหนดนามแฝงประเภทจะแสดงถึงฟังก์ชันตัวสร้างต่อไปนี้โดยอัตโนมัติ (รหัสเทียม):
    Person : String -> Int -> { name : String, age : Int }
    สิ่งนี้มีประโยชน์เช่นเมื่อคุณต้องการเขียนตัวถอดรหัส Json

    personDecoder : Json.Decode.Decoder Person
    personDecoder =
        Json.Decode.map2 Person
            (Json.Decode.field "name" Json.Decode.String)
            (Json.Decode.field "age" Int)
    


  2. ระบุฟิลด์ที่จำเป็น
    บางครั้งพวกเขาเรียกว่า "บันทึกที่ขยายได้" ซึ่งอาจทำให้เข้าใจผิดได้ ไวยากรณ์นี้สามารถใช้เพื่อระบุว่าคุณกำลังคาดหวังว่าจะมีบางระเบียนที่มีฟิลด์เฉพาะอยู่ เช่น:

    type alias NamedThing x =
        { x
            | name : String
        }
    
    showName : NamedThing x -> Html msg
    showName thing =
        Html.text thing.name
    

    จากนั้นคุณสามารถใช้ฟังก์ชันด้านบนเช่นนี้ (ตัวอย่างเช่นในมุมมองของคุณ):

    let
        joe = { name = "Joe", age = 34 }
    in
        showName joe
    

    คำพูดของ Richard Feldman เกี่ยวกับ ElmEurope 2017อาจให้ข้อมูลเชิงลึกเพิ่มเติมว่าเมื่อใดควรใช้สไตล์นี้

  3. การเปลี่ยนชื่อสิ่งต่างๆ
    คุณอาจทำได้เนื่องจากชื่อใหม่อาจให้ความหมายเพิ่มเติมในภายหลังในรหัสของคุณเช่นในตัวอย่างนี้

    type alias Id = String
    
    type alias ElapsedTime = Time
    
    type SessionStatus
        = NotStarted
        | Active Id ElapsedTime
        | Finished Id
    

    บางทีอาจจะเป็นตัวอย่างที่ดีของชนิดของการใช้งานในหลักนี้อยู่Time

  4. การเปิดเผยประเภทจากโมดูลอื่นอีกครั้ง
    หากคุณกำลังเขียนแพ็คเกจ (ไม่ใช่แอปพลิเคชัน) คุณอาจต้องใช้ประเภทในโมดูลเดียวบางทีอาจอยู่ในโมดูลภายใน (ไม่เปิดเผย) แต่คุณต้องการแสดงประเภทจาก โมดูลอื่น (สาธารณะ) หรือคุณต้องการเปิดเผยประเภทของคุณจากหลายโมดูล
    TaskในหลักและHttp.Request ใน Httpเป็นตัวอย่างสำหรับครั้งแรกในขณะที่Json.Encode.ValueและJson.Decode.Valueคู่เป็นตัวอย่างของในภายหลัง

    คุณสามารถทำได้ก็ต่อเมื่อคุณต้องการให้ประเภททึบแสง: คุณจะไม่เปิดเผยฟังก์ชันตัวสร้าง ดูรายละเอียดการใช้งานtypeด้านล่าง

เป็นที่น่าสังเกตว่าในตัวอย่างข้างต้นมีเพียง # 1 เท่านั้นที่มีฟังก์ชันตัวสร้าง หากคุณแสดงนามแฝงประเภทของคุณใน # 1 เช่นmodule Data exposing (Person)นั้นจะแสดงชื่อประเภทเช่นเดียวกับฟังก์ชันตัวสร้าง



ประเพณีของ type

  1. กำหนดประเภทสหภาพที่ติดแท็ก
    นี่คือกรณีการใช้งานที่พบบ่อยที่สุดตัวอย่างที่ดีคือMaybeประเภทในแกน :

    type Maybe a
        = Just a
        | Nothing
    

    เมื่อคุณกำหนดประเภทคุณยังกำหนดฟังก์ชันตัวสร้างด้วย ในกรณีที่อาจจะเป็น (รหัสหลอก):

    Just : a -> Maybe a
    
    Nothing : Maybe a
    

    ซึ่งหมายความว่าหากคุณประกาศค่านี้:

    mayHaveANumber : Maybe Int
    

    คุณสามารถสร้างได้โดย

    mayHaveANumber = Nothing
    

    หรือ

    mayHaveANumber = Just 5
    

    JustและNothingแท็กไม่เพียง แต่ทำหน้าที่เป็นฟังก์ชั่นคอนสตรัคพวกเขายังทำหน้าที่เป็น destructors หรือรูปแบบในcaseการแสดงออก ซึ่งหมายความว่าการใช้รูปแบบเหล่านี้คุณสามารถเห็นได้ภายใน a Maybe:

    showValue : Maybe Int -> Html msg
    showValue mayHaveANumber =
        case mayHaveANumber of
            Nothing ->
                Html.text "N/A"
    
            Just number ->
                Html.text (toString number)
    

    คุณสามารถทำได้เนื่องจากโมดูลอาจถูกกำหนดไว้เช่น

    module Maybe exposing 
        ( Maybe(Just,Nothing)
    

    มันยังสามารถพูดได้

    module Maybe exposing 
        ( Maybe(..)
    

    ทั้งสองมีความเท่าเทียมกันในกรณีนี้ แต่การพูดอย่างชัดเจนถือเป็นคุณธรรมใน Elm โดยเฉพาะอย่างยิ่งเมื่อคุณเขียนแพ็คเกจ


  1. การซ่อนรายละเอียดการใช้งาน
    ตามที่ระบุไว้ข้างต้นเป็นตัวเลือกโดยเจตนาที่Maybeจะมองเห็นฟังก์ชันตัวสร้างสำหรับโมดูลอื่น ๆ

    อย่างไรก็ตามมีกรณีอื่น ๆ เมื่อผู้เขียนตัดสินใจที่จะซ่อนไว้ หนึ่งในตัวอย่างนี้ในหลักคือ Dictในฐานะผู้ใช้แพ็กเกจคุณไม่ควรเห็นรายละเอียดการใช้งานอัลกอริทึมต้นไม้สีแดง / ดำที่อยู่เบื้องหลังDictและยุ่งกับโหนดโดยตรง การซ่อนฟังก์ชันตัวสร้างจะบังคับให้ผู้บริโภคของโมดูล / แพ็กเกจของคุณสร้างเฉพาะค่าประเภทของคุณ (จากนั้นแปลงค่าเหล่านั้น) ผ่านฟังก์ชันที่คุณเปิดเผย

    นี่คือเหตุผลว่าทำไมบางครั้งสิ่งนี้จึงปรากฏในโค้ด

    type Person =
        Person { name : String, age : Int }
    

    ซึ่งแตกต่างจากtype aliasคำจำกัดความที่ด้านบนของโพสต์นี้ไวยากรณ์นี้จะสร้างประเภท "union" ใหม่โดยมีฟังก์ชันตัวสร้างเพียงฟังก์ชันเดียว แต่ฟังก์ชันตัวสร้างนั้นสามารถซ่อนจากโมดูล / แพ็กเกจอื่น ๆ ได้

    หากประเภทถูกเปิดเผยเช่นนี้:

    module Data exposing (Person)
    

    เฉพาะโค้ดในDataโมดูลเท่านั้นที่สามารถสร้างค่าบุคคลและมีเพียงรหัสนั้นเท่านั้นที่สามารถจับคู่รูปแบบได้


1

ความแตกต่างหลักอย่างที่ฉันเห็นคือตัวตรวจสอบประเภทจะตะโกนใส่คุณหรือไม่ถ้าคุณใช้ประเภท "synomical"

สร้างไฟล์ต่อไปนี้วางไว้ที่ไหนสักแห่งแล้วเรียกใช้elm-reactorจากนั้นไปhttp://localhost:8000ดูความแตกต่าง:

-- Boilerplate code

module Main exposing (main)

import Html exposing (..)

main =
  Html.beginnerProgram
    {
      model = identity,
      view = view,
      update = identity
    }

-- Our type system

type alias IntRecordAlias = {x : Int}
type IntRecordType =
  IntRecordType {x : Int}

inc : {x : Int} -> {x : Int}
inc r = {r | x = .x r + 1}

view model =
  let
    -- 1. This will work
    r : IntRecordAlias
    r = {x = 1}

    -- 2. However, this won't work
    -- r : IntRecordType
    -- r = IntRecordType {x = 1}
  in
    Html.text <| toString <| inc r

หากคุณไม่2.แสดงความคิดเห็นและแสดงความคิดเห็น1.คุณจะเห็น:

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

34|                              inc r
                                     ^
Function `inc` is expecting the argument to be:

    { x : Int }

But it is:

    IntRecordType

0

An aliasเป็นเพียงชื่อที่สั้นกว่าสำหรับประเภทอื่นที่คล้ายกันclassใน OOP ประสบการณ์:

type alias Point =
  { x : Int
  , y : Int
  }

type(ไม่มีนามแฝง) จะช่วยให้คุณกำหนดประเภทของคุณเองเพื่อให้คุณสามารถกำหนดประเภทเช่นInt, String... สำหรับคุณแอป สำหรับ exmaple ในกรณีทั่วไปสามารถใช้สำหรับคำอธิบายสถานะของแอป:

type AppState = 
  Loading          --loading state
  |Loaded          --load successful
  |Error String    --Loading error

ดังนั้นคุณสามารถจัดการมันได้อย่างง่ายดายในviewเอล์ม:

-- VIEW
...
case appState of
    Loading -> showSpinner
    Loaded -> showSuccessData
    Error error -> showError

...

ฉันคิดว่าคุณทราบความแตกต่างระหว่างและtypetype alias

แต่ทำไมและวิธีการใช้งานtypeและtype aliasมีความสำคัญกับelmแอปพวกคุณสามารถอ้างอิงบทความจาก Josh Clayton

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.