Делая свой тип.
WelcomeВновь, toдобро theпожаловать thirdна andсерию finalОтрыв partПонедельнечного ofХаскельного ourУтра! Заключительная часть. На случай, если вы прпоустили 2 прошлые главы. В первой части мы обсудили базовую установку Haskell liftoffплатформы. series!Затем Inокунулись caseв youнаписание missedбазовых them,выражений here are the links to part 1 and part 2. In part 1 covered the basics of installing theна Haskell platform.в Thenинтерпретаторе. weВо dugвторой intoчасти, writingмы someначали basicс Haskellнаписания expressionsнашей inсобственной theфункции interpreter.в Inмодуле partHaskell. 2,Так weже startedизучили writingвсяких ourсинтаксических ownуловок functionsдля inпостроения Haskellбольших modules.и Weулучшенных also learned a lot of cool syntax tricks to build bigger and better functions.функций.
NowВ inтретьей partчасти three,мы we'reсобираемся goingуглубиться toв wrapсистемы upтипов. byИзучим goingкак moreсоздавать inсвои depthтипы withданных, theа typeтак system.же We'reхитрости goingдля toупрощения learnописания howнаших to build our own types. We'll also learn some interesting tricks to make it easier to describe our types. Once you're done with this article, you should download our Haskell Beginner's Checklist! It will point you to some other tools and resources to help you further hone your skills. It also goes over all the main language concepts we learned in this series!
If you want to take these skills and learn how to make a Haskell project with them, you should also check out our Stack Mini-Course as well!типов.
MakingСозание aнового Newтипа Data Typeданных
NowВперед, onк toтипам dataданных! types!Помните, Rememberчто thatу weнас haveесть agithub Githubрепозиторий Repositoryгде whereвы youможете canполучить followкод theдля codeэтой inчасти. thisЕсли part!вы Ifхотите youреализовать wantего toсамостоятельно, implementвы theможете codeперейти yourself,к youмодулю canDataTypes. goНо toесли theвы DataTypesпросто module.хотите Butпосмотреть ifна youзавершенный justкод, wantвы toможете lookвзглянут at the complete code as a reference, you can check outна DataTypesComplete.
ForДля thisэтой article,статьи, let'sпредскавим. supposeчто we'reмы tryingпытаемся toсмоделировать model someone'sнекий TODO list.список. We'llВ createэтой severalстатье differentсоздадим несколько различных Task
dataтипов typesданных toдля representотражения eachотдельных individualзадач taskв onсписке. theirСоздадим listтип throughoutданных thisсначала article.у Weкоторого createбудет aключевое dataслово typeи byзатем firstимя usingтипа. theЗатем dataдобавим keywordоператор andприсваивания following it up with the type name. Then we'll add the =
. assignment operator:
module DataTypes where
data Task1 = ...
NoticeВ thatотличии unlikeот theвыражения expressionsи andфункции functionименя namesкоторые weмы usedиспользовали inв theранее, previousнаши lessons,типы ourначинаются typeс startsзаглавной withбуквы. aЭто capitalто letter.что Thisотличает isтипы whatот distinguishesобычных typesвыражений from normal expressions inв Haskell. We'reТеперь nowсобираемся goingсоздать toнаш makeпервый ourконструктор. firstЭто constructor.специальный Aтип constructorвыражения, isкоторый aпозволяет specialнам typeсоздавать ofобъект expressionнашего thatтипа allowsTask
. usОни toимеют createсхожесть anс objectконструкторами ofскажем our Task type. They have some similarities to constructors in, say,на Java. ButНо they'reони alsoони veryтак different.же Constructorsочень have an uppercase name, and then they have a list of types. This list of types is the information contained by that constructor. In our case, we want our task to have a name, and an expected length of time (in minutes)сложны. We'llКонструкторы representимеют theЗаглавные nameбуквы withа aтак string,же andсписок theтипов. lengthЭтот ofсписок timeтипов withсодержит anинформацию Int.которую хранит конструктор. В нашем случае, мы хотим, чтобы наша задача имела имя и ожидаемоее время выполнения в минутах, отражены как String, и Int соответственно.
data Task1 = BasicTask1 String Int
AndВот justтак, likeтеперь that,мы weможем canначать now start makingсоздавать Task objects!объекты. ForНапример, instance,давайте let'sопределим defineпару aпростых coupleзадач basicкак tasksвыражения asв expressionsнашем within our module:модуле.
assignment1 :: Task1
assignment1 = BasicTask1 "Do assignment 1" 60
laundry1 :: Task1
laundry1 = BasicTask1 "Do Laundry" 45
WeМы couldможем alsoзагрузить loadнаш upкод ourв codeинтерпретатор, inчтоы theпроверить interpreterчто toон checkсобирается thatи itимеет still compiles and makes sense:смысл:
>> :l MyData.hs
>> :t assignment1
assignment1 :: Task1
>> :t laundry1
laundry1 :: Task
NoticeОтметим, thatчто theтип typeнашего ofвыражения ourTask1
expressionдаже isне Task1смотря, evenчто thoughмы weсобираемся constructобъекты theиспользуя objectsBasicTask1Constructor
. using the BasicTask1constructor. Now inВ Java, weможно canиметь haveмножество manyконструкторов constructorsдля forодного theтипа. sameМы type.можем Weсделать canтак alsoже doи thisв inHaskell, Haskellно butвыглядит itэто looksпо aсложнее. littleДавайте different.определим Let'sдругой defineтип anotherдля typeразличных forмест, theгде differentмы locationsможем whereработать weнад canзадачами. performМы aможем task.производить Weработу couldнад performзадачами aв Taskшколе, atофисе, school,дома. theОтразим office,это orсоздава atконструктор home.для We'llкаждого representиз thisних. byРазделим creatingконструктор aиспользуя constructorвертикальную forчерту each of these. We separate the constructors using the vertical bar |
:
data Location =
School |
Office |
Home
InВ thisэтом case,случае, eachкаждый ofиз theконструкторов constructorsпростая isотметка, aкоторая simpleне markerимеет thatпараметров hasили noданных parametersхранящихся orв dataнем. storedЭто withinпример it.Enum
Thisтипа. isМы anможем exampleтехнически ofсделать anразличные "Enum"типы type.выражения Weотражающими canкаждый technicallyиз make different types of expressions representing each of these:них.
schoolLocation :: Location
schoolLocation = School
officeLocation :: Location
officeLocation = Office
homeLocation :: Location
homeLocation = Home
ButНо theseэти expressionsвыражения aren'tне anyболее moreполезны usefulчем thanиспользовать usingсами the constructors themselves.конструкторы.
NowТеперь, thatимея weпару haveтипов, aмы coupleможем differentсделать types,так, weчто canодин actuallyиз haveнаших oneтипов ofбудет ourсодержать typesдругие! containДобавим theновый other!конструктор We'llв addнаш aтип newзадач. constructorЭто toбудет ourеще taskсложнее type.чем Itпросто willсписок represent a more complicated task that also lists a location:мест.
data Task1 =
BasicTask1 String Int |
ComplexTask1 String Int Location
...
complexTask :: Task1
complexTask = ComplexTask1 "Write Memo" 30 Office
SoЭто thisсильно isотличается veryот differentконструктора fromв constructorsдругих inязыках. otherМы language.можем Weиметь canразличные actuallyполя haveдля differentразличных fieldsотображений forтипов. differentМожно representationsобернуть ofсовершенно ourотличающийся type.тип Weзависящий canот wrapконструктора completelyкоторый differentмы dataиспользуем. dependingЭто onотлично, theтак constructorкак weдает use.нам Thisгибкость, isкоторую awesomeругие andязыке givesне us a lot of flexibility that other languages struggle to give us.могут.
ParameterizedПараметризированные Typesтипы
AnotherЕще coolиспользовать thingпараметризированные weтипы canс doдругими withопределениями ourтипов. typeЭто definitionsзначит, isчто toодин useили typeболее parameters.полей Thisзависят meansот thatтипа, oneкоторый orбыл moreвыбран ofчеловеком theкоторый fieldsписал actuallyкод. dependsДавайте onпредположим, aу typeнас thatесть theтип, personкоторый writingимеет theнесколько codeбазовых getsконструкторов toдля select.различных Let'sвидов supposeвремени. weЭто haveограничит aнаше typeописание thatдля has a few basic constructors for different amounts of time. This would restrict our description of the time for the sake of simplicity.простоты.
data TaskLength =
QuarterHour |
HalfHour |
ThreeQuarterHour |
Hour |
HourAndHalf |
TwoHours |
ThreeHours
NowТеперь weмы mightхотим wantописать toзадачу describeгде aвремя taskзадачи whereбудет theвыражатся length of the task is anв Int. ButНо weтак mightже alsoхотим, wantчтобы aбыла taskвозможность toописать beс ableпомощью toнового useтипа. thisДавайте newсделаем taskвторую lengthверси type.нашего Let'sTask
makeтипа, aкоторый secondможет versionиспользовать ourоба ourтипа Taskдля typeвремени thatвыполнения. canМы useможем eitherсделать typeэто forс theпомощью length.параметризованного We can do this by parameterizing the type like so:типа:
data Task2 a =
BasicTask2 String a |
ComplexTask2 String a Location
TheТип typeстал aмистическим, isи nowтеперь aмы mysteryможем typeего thatзаполнять weкак canхотим. fillНо inтеперь asпри weвыводе please.Task2
Butтипа nowв wheneverсигнатуре, weмы listдолжны theбудет Task2заполнить typeправильное in a type signature, we have to fill in the proper definition:определение.
assignment2 :: Task2 Int
assignment2 = BasicTask2 "Do assignment 2" 60
assignment2' :: Task2 TaskLength
assignment2' = BasicTask2 "Do assignment 2" Hour
laundry2 :: Task2 Int
laundry2 = BasicTask2 "Do Laundry" 45
laundry2' :: Task2 TaskLength
laundry2' = BasicTask "Do Laundry" ThreeQuarterHour
complexTask2 :: Task2 TaskLength
complexTask2 = ComplexTask2 "Write Memo" HalfHour Office
WeК haveэтом toнужно beотносится carefulс though,осторожностью, sinceтак thisкак canэто restrictможет ourограничить abilityнашу toвозможность doделать certainопределнные things.вещи. ForНапример, instance,мы weне cannotможем createсоздать aсписок, listкоторый thatсодержит containsоба bothи assignment2
andи complexTask2.complexTask2
. ThisЭто isпотому, becauseчто theдва twoвыражения expressionsтеперь nowразличные have different types!типы.
-- THIS WILL CAUSE A COMPILER ERROR
badTaskList :: [Task2 a]
badTaskList = [assignment2, complexTask2]
ListПример Exampleсписка
SpeakingГоворя ofо lists,списках, weмы canможем actuallyприоткрыть unravelзавесу aтайны bitо ofтом, theкак mysteryсписки about how lists are implemented now.реализованны.
ThereБольшое isколичество aсинтаксического lotсахара ofменяют syntacticспособ sugarнаписания thatсписка changesна howпрактике. weНо actuallyна writeуровне listsкода, inсписки practice.определяются Butдвумя atконструкторами, theNil
sourceи level, lists are defined by two constructors, Nil and Cons.Cons
.
data List a =
Nil |
Cons a (List a)
AsКак weмы shouldожидаем, expect,тип theList
Listимеет typeодин hasпараметр. aЭто singleто typeчто parameter.позволяет Thisнам isодновременно whatиметь allowsInt
usили toString
. eitherКонструктор haveNil
[Int]это orпустой [String]список.The NilНе constructorсодержит isобъектов. anПоэтому emptyв list.любое Itвремя, containsв noкоторое objects.вы Soбудете anyиспользовать time you're using theвыражение [], expression,занайте you'reвы actuallyиспользуете usingNil
. Nil.Второй Thenконструктор theскладывает secondодин constructorэлемент concatenatesс aдругим singleсписком. elementТип withэлемента anotherи list.списка Theдолжны, typeконечно ofже theсовпадать. elementПри andиспользовании the:
listоператора mustдля matchдобавления upэлемента obviously.в Whenсписок, youвы useуже theиспользуете :Cons
operator to prepend an element to a list, you are really using the Cons constructor.конструктор.
emptyList :: [Int]
emptyList = [] -- Actually Nil
fullList :: [Int]
-- Equivalent to Cons 1 (Cons 2 (Cons 3 Nil))
-- More commonly written as [1,2,3]
fullList = 1 : 2 : 3 : []
AnotherЕще coolодна thingвещь, hereто isчто thatнаша ourструктура dataданных structureрекурсивна. isМы recursive.можем Weувидеть canв seeCons
inконструкторе theкак Consсписок constructorсодержит howдругой aсписок listс containsпараметрами. anotherЭто listРаботает asотлично, aпокоа parameter.есть Thisкакой-то worksбазовый fineслучай! asТогда, fineу asнас longбудет asNil
. there'sПредставьте someесли baseу case!нас Inесть thisодин situation,конструктор weи haveон Nil.принимает Imagineрекурсивный ifпараметр. weУ onlyнас hadвозникает aзатруднительное singleположение, constructorиз-за andтого, itчто tookмы aне recursiveзнаем parameter.как We'dсоздать beлюбойс inписок aна realпервом pickle about how we create any list in the first place!месте.
RecordСинтаксическая Syntaxзаписи
SoДавайте let'sвернемся goк backосновам, toнепараметризированному ourтипу basic,данных unparameterizedTask
. Предположим, нас не волнует в целом объект Task
. dataСкорее, type.мы Supposeхотим weодин don'tиз careего aboutкусочков, theнапиример entireимя Taskили item.время. Rather,Так weкак wantнаш oneкод of- itsединственный pieces,способ likeсделать theэто nameиспользовать orсопоставление time.с Asобразцом ourкоторый codeявит isнужное now, the only real way to do that is to use a pattern match that reveals these fields.поле.
import Data.Char (toUpper)
...
twiceLength :: Task1 -> Int
twiceLength (BasicTask1 name time) = 2 * time
capitalizedName :: Task1 -> String
capitalizedName (BasicTask1 name time) = map toUpper name
tripleTaskLength :: Task1 -> Task1
tripleTaskLength (BasicTask1 name time) = BasicTask1 name (3 * time)
NowТеперь weслегка canупрощаем. simplifyВы thisможете aиспользовать teensyнижнее bit.подчеркивание Youвместо canпараметра, useкоторый underscoresвы insteadне ofхотите parametersисопльзовать. thatНо youнесмотря won'tна use.это, Butможет evenполучится so,громоздко thisесли canу getваш veryтип cumbersomeимеет ifножество youполей. haveМы aможем dataнаписать typeнашу thatфункцию hasпозволяющую aиметь lotдоступ ofк fields.отдельным Weполям. couldПод writeкапотом, ourконечно ownже, functionsбудет allowingсопоставление usс to access individual fields. Of course, these will have to use pattern matching under the hood:образом.
taskName :: Task1 -> String
taskName (BasicTask1 name _) = name
taskLength :: Task1 -> Int
taskLength (BasicTask1 _ time) = time
twiceLength :: Task1 -> Int
twiceLength task = 2 * (taskLength task)
capitalizedName :: Task1 -> String
capitalizedName task = map toUpper (taskName task)
tripleTaskLength :: Task1 -> Task1
tripleTaskLength task = BasicTask1 (taskName task) (3 * (taskLength task))
ButНо thisэто approachприменение doesn'tнельзя scale,масштабировать, sinceтак we'llкак haveнам toнужно writeписать theseэту functionsфункцию forдля everyкаждого differentполя, fieldкоторое ofмы everyбудем dataсоздавать. typeТеперь weпредставьте create.насколько Nowлегко, imagineиспользовать howметод easysetter
it is to use a "setter" method inв Java. CompareСравним thatэто toс tripleTaskLength
above.выше. WeНужно haveпротись toпо re-iterateвсем mostполям, ofчто theне existingесть fields,хорошо. whichОтличная isновость, tedious.в Theтом, excitingчто newsмы isможем that we can getзаставить Haskell toнаписать writeфункцию theseдля functionsнас forиспользовать usсинтаксис usingзаписи. recordДля syntax.этого, Toвсё, doчто this,нам allнужно weэто haveназначить toкаждому doполю isв assignопределении eachнашего fieldтипа. aДавайте nameсделаем inновую ourверсию data definition. Let's make a new version of Task:Task
.
data Task3 = BasicTask3
{ taskName :: String
, taskLength :: Int }
NowТеперь weможно canписать writeтот theже sameкод codeбез WITHOUTgetter
theфункции "getter"которую functionsмы weписали wrote above.выше.
-- These will now work WITHOUT our separate definitions for "taskName" and
-- "taskLength"
twiceLength :: Task3 -> Int
twiceLength task = 2 * (taskLength task)
capitalizedName :: Task3 -> String
capitalizedName task = map toUpper (taskName task)
NowТеперь whenможно weсоздать constructзадачу, tasks,мы weвсё canеще stillможем useиспользвать theBasicTask3
BasicTask3сам constructorпо byсебе. itself.Но Butдля forчистоты codeкода, clarity,мы weможем canтак alsoже initializeсоздать theобъект objectиспользуя usingсинтаксическую recordзапись, syntax,где whereмы weназывали name the field:поле:
-- BasicTask3 "Do assignment 3" 60 would also work
assignment3 :: Task3
assignment3 = BasicTask3
{ taskName = "Do assignment 3"
, taskLength = 60 }
laundry3 :: Task3
laundry3 = BasicTask3
{ taskName = "Do Laundry"
, taskLength = 45 }
WeМы canтак alsoже writeможем aнаписать "setter"setter
moreеще easilyпроще usingиспользуя recordсинтаксическую syntax.запись. WeВопользуемся useпрошлой theзадачей previousи taskзатем andсписоком then a list ofизменений "changes" toчтобы makeпоместить withinих braces:в скобки.
tripleTaskLength :: Task3 -> Task3
tripleTaskLength task = task { taskLength = 3 * (taskLength task) }
Generally,В weобщем, onlyмы useиспользуем recordтолько syntaxсинтаксическую whenзапись, thereкогда isесть aодин singleконструктор constructorдля forтипа aданных. dataМы type.можем Weиспользовать canразличные useполя differentдля fieldsразличных forконструкторов, differentно constructors,только butнаш ourкод codeчуток becomesбезопаснее. aДавайте bitпосмотрим lessна safe.еще Let'sодин seeпример anotherопределения example Task
: definition:
data Task4 =
BasicTask4
{ taskName4 :: String,
taskLength4 :: Int }
|
ComplexTask4
{ taskName4 :: String,
taskLength4 :: Int,
taskLocation4 :: Location }
The trouble with this system is that the compiler will generate a taskLocation4 function that will compile for any task. But the function will only be valid when called on a ComplexTask4. So the following code will compile, even though it will cause a crash, and we want to avoid that:
causeError :: Location
causeError = taskLocation4 (BasicTask4 "Cause error" 10)
In addition, if our different constructors use different types, we can't use the same name for them. This can be frustrating when we want to represent the same concept with different types. This example won't compile because GHC cannot determine the type of the taskLength4 function. It could either have type Task -> Int or Task -> TaskLength.
data Task4 =
BasicTask4
{ taskName4 :: String,
taskLength4 :: Int }
|
ComplexTask4
{ taskName4 :: String,
taskLength4 :: TaskLength, -- Note we use "TaskLength" and not an Int here!
taskLocation4 :: Location }
The Type Keyword
Now we know most of the ins and outs of making our own data types. But there are times when you don't need to do this. We can create new type names without making a completely new data structure. There are two ways to do this. The first is the type keyword. It allows you to create a synonym for a type, like the typedef keyword in C++. The most common of these, as we've seen, is that a String is actually a list of characters:
type String = [Char]
A common use case for this is when you've combined many different types together in a tuple. It can be quite tedious to write this tuple down several times in your code:
makeTupleBigger :: (Int, String, Task) -> (Int, String, Task)
makeTupleBigger (intValue, stringValue, (BasicTask name time) =
(2 * intValue, map toUpper stringValue, (BasicTask (map toUpper name) (2 * time)))
A type synonym would make the signature here look a lot cleaner:
type TaskTuple = (Int, String, Task)
makeTupleBigger :: TaskTuple -> TaskTuple
makeTupleBigger (intValue, stringValue, (BasicTask name length) =
(2 * intValue, map toUpper stringValue, (BasicTask (map toUpper name) (2 * length))
Of course, if this collection of items shows up a lot, it might be worth making a full data type for it. There are also some reasons why type synonyms aren't always the best choice. For one thing, they can lead to compile errors that can be difficult to work through. You've probably come across a few errors already where the compiler told you it expected a [Char]. It would have been far more clear if it had said String.
It can also lead to some unintuitive code. Suppose you use a basic tuple instead of a data type to represent a Task. Someone might expect your Task type to be its own data type. Then they'll be a little confused when you manipulate it like a tuple:
type Task5 = (String, Int)
twiceTaskLength :: Task5 -> Int
-- "snd task" is confusing here
twiceTaskLength task = 2 * (snd task)
Newtypes
The last topic we'll cover is "newtypes". These are like type synonyms in some ways, and ADTs in other ways. But they still have a unique place in Haskell and it is good to get accustomed to using them. Let's suppose we want to have a new approach to representing TaskLength. We want to use a regular number, but we want it to have its own separate type. We can do this using "newtype":
newtype TaskLength2 = TaskLength2 Int
The syntax for newtypes looks a lot like defining an ADT. However, a newtype definition can only have a single constructor. And that constructor can only take a single type argument. The big difference between an ADT and a newtype comes after your code is compiled. In this example, there won't be a difference between the TaskLength and Int types at runtime. This is good because a lot of code for Int types is specialized to run fast. If we were to make this a true ADT, this would not be the case:
-- Not as fast!
data TaskLength2 = TaskLength2 Int
But otherwise, we can do a lot of the same tricks with our newtype that we can do with ADTs. We can, for instance, use record syntax in the constructor for our newtype. This allows us to use a name to unwrap the inside value without pattern matching on the type. A frequent pattern when using record syntax is to use something like "un-TypeName" value as the field name. Also note that we can't use the newtype value with the same functions as the original type. When we had type synonyms, we could do this, but it won't here:
data Task6 = BasicTask6 String TaskLength2
newtype TaskLength2 = TaskLength2
{ unTaskLength :: Int }
mkTask :: String -> Int -> Task6
mkTask name time = BasicTask6 name (TaskLength2 time)
twiceLength :: Task6 -> Int
twiceLength (BasicTask6 _ len) = 2 * (unTaskLength len)
-- The following would be WRONG!
-- 2 *len
Now, TaskLength2 is effectively a wrapper type around an Int. This makes it seem a lot like a type synonym, except that we can't simply use the Int value itself. As you can see in the examples above, we do have to go through the process of wrapping and unwrapping the value. This seems tedious. But it is quite useful because it solves the main problems we've seen from using type synonyms. Now if we make a mistake involving TaskLength, the compiler will tell us it's about TaskLength. We won't be wondering if there's a synonym we're missing!
Here's another example. Suppose we have a function with several integral arguments. If we always use Int types, we can easily confuse the order of the arguments. But when we use a newtype, the compiler will catch this type of error for us.
Conclusion
This wraps up our discussion on creating your own data types and is the conclusion of our Liftoff Series! If you need a refresher, don't forget to check out part 1 and part 2 to refresh yourself on the basics. For some more resources on learning Haskell, download our free Beginner's Checklist! You'll be able to review all the concepts you learned in this series. The checklist will also tell you about some tools that will streamline your Haskell workflow!
If you want to take the next step in your Haskell education, you should check out our Stack Mini-Course. This short video course walk you through how to use Stack and the Haskell platform to start making your own Haskell project!