Elm Language
Типы, переменные типа и конструкторы типов
Поиск…
замечания
Пожалуйста, играйте с этими концепциями сами, чтобы действительно овладеть ими! 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, но без возможности для пользователя определить их.