Поиск…


замечания

Пожалуйста, играйте с этими концепциями сами, чтобы действительно овладеть ими! elm-repl (см. Введение к REPL ), вероятно, является хорошим местом для игры с кодом выше. Вы также можете играть с elm-repl онлайн .

Сопоставимые типы данных

Сопоставимые типы - это примитивные типы, которые можно сравнить с помощью операторов сравнения из модуля Basics , например: (<) , (>) , (<=) , (>=) , max , min , compare

Сопоставимыми типами в Elm являются Int , Float , Time , Char , String и кортежи или списки сопоставимых типов.

В документах или определениях типов они называются специальной переменной типа, comparable , например. см. определение типа для функции Basics.max :

max : comparable -> comparable -> comparable

Подписи типов

В Elm значения объявляются путем записи имени, знака равенства, а затем фактического значения:

someValue = 42

Функции также являются значениями, с добавлением значения или значений в качестве аргументов. Они обычно записываются следующим образом:

double n = n * 2

Каждое значение в Elm имеет тип. Типы значений выше будут выведены компилятором в зависимости от того, как они используются. Но лучше всего всегда указывать тип любого значения верхнего уровня, и для этого вы пишете подпись типа следующим образом:

someValue : Int
someValue = 
    42

someOtherValue : Float
someOtherValue =
    42

Как мы можем видеть, 42 может быть определена либо как Int или Float . Это делает интуитивный смысл, но см. Переменные типа для получения дополнительной информации.

Типичные подписи особенно ценны при использовании с функциями. Вот функция удвоения:

double : Int -> Int
double n =
    n * 2

На этот раз подпись имеет a -> , стрелку, и мы произносим подпись как «int to int», или «принимает целое число и возвращает целое число». -> указывает, что, давая double значение Int как аргумент, double вернет Int . Следовательно, он принимает целое число в целое число:

> double
<function> : Int -> Int

> double 3
6 : Int

Основные типы

В elm-repl введите кусок кода, чтобы получить его значение и предполагаемый тип. Попробуйте следующее, чтобы узнать о различных существующих типах:

> 42
42 : number

> 1.987
1.987 : Float

> 42 / 2
21 : Float

> 42 % 2
0 : Int

> 'e'
'e' : Char

> "e"
"e" : String

> "Hello Friend"
"Hello Friend" : String

> ['w', 'o', 'a', 'h']
['w', 'o', 'a', 'h'] : List Char

> ("hey", 42.42, ['n', 'o'])
("hey", 42.42, ['n', 'o']) : ( String, Float, List Char )

> (1, 2.1, 3, 4.3, 'c')
(1,2.1,3,4.3,'c') : ( number, Float, number', Float, Char )

> {}
{} : {}

> { hey = "Hi", someNumber = 43 }
{ hey = "Hi", someNumber = 43 } : { hey : String, someNumber : number }

> ()
() : ()

{} - пустой тип записи, а () - пустой тип Tuple. Последнее часто используется для целей ленивой оценки. См. Соответствующий пример в « Функции и частичное приложение» .

Обратите внимание на то, как number выглядит некапитализированным. Это указывает на то, что это переменная типа , и, кроме того, конкретный number слова относится к переменной специального типа, которая может быть либо Int либо Float (более подробно см. Соответствующие разделы). Типы, хотя всегда имеют верхний регистр, такие как Char , Float , List String и т. Д.

Переменные типа

Переменные типа - это некапитализированные имена в подписях типов. В отличие от своих капитализированных копий, таких как Int и String , они не представляют собой один тип, а скорее любой тип. Они используются для написания общих функций, которые могут работать на любом типе или типах, и особенно полезны для записи операций над контейнерами, такими как List или Dict . Функция List.reverse , например, имеет следующую подпись:

reverse : List a -> List a

Это означает, что он может работать над списком любого значения типа , поэтому List Int , List (List String) , оба из них и любые другие могут быть reversed . Следовательно, a - это переменная типа, которая может стоять для любого типа.

reverse функция могла использовать любое некапитализированное имя переменной в своей сигнатуре типа, за исключением нескольких специальных имен переменных типа , таких как number (для получения дополнительной информации см. Соответствующий пример):

reverse : List lol -> List lol

reverse : List wakaFlaka -> List wakaFlaka

Имена переменных типа становятся значимыми только тогда, когда существуют разные переменные типа в пределах одной подписи, примером которой является функция map в списках:

map : (a -> b) -> List a -> List b

map принимает некоторую функцию от любого типа a до любого типа b вместе со списком с элементами некоторого типа a и возвращает список элементов некоторого типа b , который он получает, применяя данную функцию к каждому элементу списка.

Давайте сделаем подпись конкретной, чтобы лучше это увидеть:

plusOne : Int -> Int
plusOne x = 
    x + 1

> List.map plusOne
<function> : List Int -> List Int

Как видим, в этом случае как a = Int и b = Int . Но если у map была подпись типа, например map : (a -> a) -> List a -> List a , то она будет работать только на функциях, работающих на одном типе, и вы никогда не сможете изменить тип списка с помощью функции map . Но так как сигнатура типа map имеет несколько разных переменных типа, a и b , мы можем использовать map для изменения типа списка:

isOdd : Int -> Bool
isOdd x =
    x % 2 /= 0

> List.map isOdd
<function> : List Int -> List Bool

В этом случае a = Int и b = Bool . Следовательно, чтобы иметь возможность использовать функции, которые могут принимать и возвращать разные типы, вы должны использовать разные переменные типа.

Псевдонимы типа

Иногда мы хотим дать типу более описательное имя. Скажем, наше приложение имеет тип данных, представляющий пользователей:

{ name : String, age : Int, email : String }

И наши функции у пользователей имеют подписи типов по строкам:

prettyPrintUser : { name : String, age : Int, email : String } -> String

Это может стать довольно громоздким с большим типом записи для пользователя, поэтому давайте использовать псевдоним типа, чтобы сократить размер и дать более значимое имя этой структуре данных:

type alias User =
    { name: String
    , age : Int
    , email : String
    }


prettyPrintUser : User -> String

Типичные псевдонимы делают его более чистым для определения и использования модели для приложения:

type alias Model =
    { count : Int
    , lastEditMade : Time
    }

Использование type alias буквально просто псевдонизирует тип с именем, которое вы ему даете. Использование вышеуказанного типа Model точно так же, как с использованием { count : Int, lastEditMade : Time } . Вот пример, показывающий, как псевдонимы не отличаются от базовых типов:

type alias Bugatti = Int

type alias Fugazi = Int

unstoppableForceImmovableObject : Bugatti -> Fugazi -> Int
unstoppableForceImmovableObject bug fug =
    bug + fug

> unstoppableForceImmovableObject 09 87
96 : Int

Алиас типа для типа записи определяет функцию-конструктор с одним аргументом для каждого поля в порядке объявления.

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

Point 3 7
{ x = 3, y = 7 } : Point

type alias Person = { last : String, middle : String, first : String }

Person "McNameface" "M" "Namey"
{ last = "McNameface", middle = "M", first = "Namey" } : Person

Каждый псевдоним типа записи имеет свой собственный порядок полей даже для совместимого типа.

type alias Person = { last : String, middle : String, first : String }
type alias Person2 = { first : String, last : String, middle : String }

Person2 "Theodore" "Roosevelt" "-"
{ first = "Theodore", last = "Roosevelt", middle = "-" } : Person2

a = [ Person "Last" "Middle" "First", Person2 "First" "Last" "Middle" ]
[{ last = "Last", middle = "Middle", first = "First" },{ first = "First", last = "Last", middle = "Middle" }] : List Person2

Улучшение безопасности типов с использованием новых типов

Типы псевдонимов разрезают на шаблоны и улучшают читаемость, но он не более безопасен по типу, чем сам тип псевдонимов. Рассмотрим следующее:

type alias Email = String

type alias Name = String

someEmail = "[email protected]"

someName = "Benedict"

sendEmail : Email -> Cmd msg
sendEmail email = ...

Используя приведенный выше код, мы можем написать sendEmail someName , и он будет скомпилирован, хотя это действительно не так, потому что, несмотря на то, что имена и электронные письма как String s, они совершенно разные.

Мы можем по-настоящему отличить одну String от другой String на уровне типа, создавая новый тип . Вот пример, который переписывает Email как type а не type alias :

module Email exposing (Email, create, send)

type Email = EmailAddress String

isValid : String -> Bool
isValid email = 
  -- ...validation logic

create : String -> Maybe Email
create email =
    if isValid email then
        Just (EmailAddress email)
    else
        Nothing

send : Email -> Cmd msg
send (EmailAddress email) = ...

Наша функция isValid делает что-то, чтобы определить, является ли строка допустимым адресом электронной почты. create функции проверку , если данная String является действительным адресом электронной почты, возвращающей Maybe -wrapped Email , чтобы гарантировать , что мы возвращать только проверенные адреса. Хотя мы можем обойти проверку валидации, EmailAddress "somestring" Email напрямую, написав EmailAddress "somestring" , если наше объявление модуля не раскрывает конструктор EmailAddress , как показано здесь

module Email exposing (Email, create, send)

то ни один другой модуль не будет иметь доступ к конструктору EmailAddress , хотя они все равно могут использовать тип Email в аннотациях. Единственный способ создать новую Email за пределами этого модуля - использовать функцию create он предоставляет, и эта функция гарантирует, что она вернет только действительные адреса электронной почты. Следовательно, этот API автоматически направляет пользователя по правильному пути посредством безопасности своего типа: send только работает со значениями, созданными create , которые выполняют проверку, и принудительно обрабатывает недействительные электронные письма, так как возвращает Maybe Email .

Если вы хотите экспортировать конструктор Email , вы можете написать

module Email exposing (Email(EmailAddress), create, send)

Теперь любой файл, который импортирует Email также может импортировать свой конструктор. В этом случае это позволит пользователям обойти проверку и send недействительные электронные письма, но вы не всегда строите API, как это, поэтому экспорт конструкторов может быть полезен. С типом, который имеет несколько конструкторов, вы также можете экспортировать некоторые из них.

Построение типов

Комбинация ключевых слов type alias дает новое имя для типа, но ключевое слово type в объявлении объявляет новый тип. Давайте рассмотрим один из самых фундаментальных из этих типов: Maybe

type Maybe a
    = Just a
    | Nothing

Первое, что нужно отметить, это то, что тип Maybe объявлен с переменной типа a . Второе, что нужно отметить, это символ трубы, | , что означает «или». Другими словами, что-то типа Maybe a либо Just a или Nothing .

Когда вы пишете вышеприведенный код, Just и Nothing попадают в область как конструкторы значений и, Maybe попадают в область видимости как конструктор типов . Это их подписи:

Just : a -> Maybe a

Nothing : Maybe a

Maybe : a -> Maybe a -- this can only be used in type signatures

Из-за переменной типа a любой тип может быть «завернут внутри» типа Maybe . Итак, Maybe Int , Maybe (List String) или Maybe (Maybe (List Html)) , все допустимые типы. При разрушении любого значения type с выражением case вы должны учитывать все возможные экземпляры этого типа. В случае значения типа Maybe a , вам нужно учитывать как Just a case, так и случай Nothing :

thing : Maybe Int
thing = 
    Just 3

blah : Int
blah =
    case thing of
        Just n -> 
            n

        Nothing ->
            42

-- blah = 3

Попробуйте написать вышеприведенный код без предложения Nothing в выражении case : он не будет компилироваться. Именно это делает конструктор Maybe типа отличным шаблоном для выражения значений, которые могут не существовать, поскольку он заставляет вас обрабатывать логику, когда значение Nothing .

Тип Never

Тип Never не может быть сконструирован (модуль Basics не экспортировал свой конструктор значений и не дал вам никакой другой функции, которая возвращает Never ). Нет значения never : Never или функция createNever : ?? -> Never .

Это имеет свои преимущества: вы можете кодировать в системе типов возможность, которая не может произойти. Это можно увидеть в таких типах, как Task Never Int которые гарантируют успех с Int ; или Program Never , которые не будут принимать никаких параметров при инициализации кода Elm из JavaScript.

Специальные типы переменных

Elm определяет следующие специальные переменные типа, которые имеют особое значение для компилятора:

  • comparable : Состоит из Int , Float , Char , String и кортежей. Это позволяет использовать операторы < и > .

    Пример. Вы можете определить функцию, чтобы найти наименьшие и самые большие элементы в списке ( extent ). Вы думаете, какую подпись типа писать. С одной стороны, вы можете написать extentInt : List Int -> Maybe (Int, Int) и extentChar : List Char -> Maybe (Char, Char) и другой для Float и String . Их реализация будет одинаковой:

    extentInt list =
      let
        helper x (minimum, maximum) = 
          ((min minimum x), (max maximum x))
      in 
        case list of 
          [] ->
            Nothing
          x :: xs ->
            Just <| List.foldr helper (x, x) xs
    

    У вас может возникнуть соблазн просто написать extent : List a -> Maybe (a, a) , но компилятор не позволит вам это сделать, потому что функции min и max не определены для этих типов (NB: это просто простые обертки вокруг оператора < упомянутого выше). Вы можете решить это, указав extent : List comparable -> Maybe (comparable, comparable) . Это позволяет вашему решению быть полиморфным , что означает, что он будет работать более чем для одного типа.

  • number : Состоит из Int и Float . Позволяет использовать арифметические операторы, кроме деления. Затем вы можете определить, например, sum : List number -> number и заставить ее работать как для int, так и для float.

  • appendable : Состоит из String , List . Позволяет использовать оператор ++ .

  • compappend : Это иногда появляется, но является деталью реализации компилятора. В настоящее время это не может быть использовано в ваших собственных программах, но иногда упоминается.

Обратите внимание, что в аннотации типа: number -> number -> number все они относятся к одному типу, поэтому передача в Int -> Float -> Int будет ошибкой типа. Вы можете решить эту проблему, добавив суффикс к имени переменной типа: number -> number' -> number'' будет компилироваться в порядке.

Официального названия для них нет, их иногда называют:

  • Специальные типы переменных
  • Типовые типы переменных типа
  • Псевдо-классы типов

Это происходит потому, что они работают как классы классов Haskell, но без возможности для пользователя определить их.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow