Please disable your adblock and script blockers to view this page

Kowainik - Haskell mini-patterns handbook


Navigating
Haskell
FP
Newtype
DerivingVia

pointY
Circle Double
|
PasswordHash
Int -> Int -> Int
ProcessName
SupervisorName
playerStrength
Int calculatePlayerDamage
Int calculateHit
hitPlayer
Int
Health ->
Defense
Description Providing
PatternSynonyms
Validation
UnsafePassword
Validation
Others
TagsList
NonEmpty
| Non
Control
Always
isNothing
UserAuth
Haskell Evidence
IntMap as
IntMap
IntMap Double
Functional Programming
Algebraic Data Types
ADT
handleTwoOptionals
->
BackendSettings
FrontendSettings
OnlyFrontend
BothSettings
Now
API
Sure
M","i","ss","i","ss","i","pp","i
Maybe (PasswordHash
PublicKey
PublicKey -> Signature
MonadFail
IO
CMD
Maybe’s
MonadComprehensions
Ints
e2 Int
Double -> Double
EQ
LinearTypes
Polymorphisation
Yellow
Ord
Implement
Fruit
Apple
OverloadedStrings
Enum
Show
Bounded
FruitName
Set.member
EvidenceReplacing
Implement partitionWith.
element).Haskell
Recursive


Int
pointX
Ivalid
ByteString -> ByteString ->

playerAttack

Int ->
calculatePlayerDefense
Int -> Int -> Int calculatePlayerDefense
Player -> Player -> Player hitPlayer player1
1🔗
Int newtype Damage
Int newtype Defense
Dexterity
Health
Data
|
mkPassword
unsafePassword
Tag String
Tag
liftA2
isNothing
Page
PasswordHash
UserAuth
UserAuth ->

BackendSettings
FrontendSettings
configureFront
| BothSettings
OnlyBackend
2🔗
| Three
UserId
Int
mkPasswordhash
Task🔗 Improve

Int -> Bool
cmdSequence
🔬 Haskell
e1 Int
Int sumThree
compareByFst
compareByFst
isNumber
parseNumber
catMaybes
Foldable
| Yellow
Green
Yellow ->
Blue ->
Relude
| Green
| Orange
| Lemon
Lemon ->
Blueberry ->
FruitName parseFruitName
fruitColour
🙅 Disclaimer

👩


SupervisorId
parseNumber
Blue
Bi
Extra
Orange -


Maybes
PrivateKey
Data.


MonadFail m
Polymorphisation🔗 Pattern
LambdaCase
BangPatterns
MonadFail sugarElegant


Int
👩
Data
^ Map
IO
MessageId
MonadFail
maybeConcatList
Orange
UserAuth
results?We
goMoving


sumThree

Positivity     41.00%   
   Negativity   59.00%
The New York Times
SOURCE: https://kowainik.github.io/posts/haskell-mini-patterns
Write a review: Hacker News
Summary

newtype is an ordinary data type with the name and a constructor. However, you can define a data type as newtype instead of data only if it has exactly one constructor with exactly one field. It is extremely easy to define a newtype in Haskell as no extra effort is required from the user compared to the data type declaration. But you can’t declare the following data types as a newtype: You can think of a newtype as a lightweight data type. Notwithstanding from the compiler point of view, different newtypes are different data types. Since both password and hash are merely textual data, we could write the following function: You can implement a more type-safe version of the validateHash function, and additionally this improves the code readability: Another popular way of increasing code readability without sacrificing performance is introducing a new alias (which is simply another name for a data type) by using the type keyword. type Defense = Int The approach of using types instead of newtype can bring even more damage when you have a lot of similar data types. Improve the following code by applying the Newtype pattern. When a data type restricts some values (e.g. not every ByteString is a valid Password). Usually you would create a value of a data type by using its constructor. It can be understood better by looking at the implementation of such approach based on the Password data type: But we need a way to deconstruct a value of type Password hence the Password (unPassword) line in the export list. You still can forget to call this validation function in the application code and allow users to work with invalid passwords. One more type-safe approach (but also a bit more heavyweight solution) would be to return some sort of witness for the fact that a password was validated, and then require this witness in the future functions. Data types precisely describe the domain allowing to create all valid values and making it impossible to construct invalid values. May require writing custom helper data types and functions. Functional Programming features such as Algebraic Data Types (ADT), parametric polymorphism and others allow describing the shape of the valid data more precisely to the extent that it is impossible to construct any invalid values. The type of handleTwoOptionals allows passing Just a and Nothing :: Maybe b, but in reality the function doesn’t process such a combination. But the above function has the same problem: the data type makes it possible to specify values that shouldn’t happen in real life. To fix this problem, we need to change the shape our Settings data type in the following way by using sum types: Generally, by pushing requirements for our data types upstream, we can implement more correct code and make the life of our API users easier, because they can’t shoot themselves in the foot by providing illegal values. But to be exactly precise in our data description, we probably need to create another enumeration type specifying only valid combinations of Answers. In this case, it is not feasible to describe only valid states with a data type, so we don’t bother doing this. Implement the following function by applying the make illegal states unrepresentable pattern. Sometimes you can improve benefits gained with the usage of newtypes even further by using “phantom type variables” — type variables that are not used after the “=” sign in the data type declaration. This approach allows you to avoid creating multiple data types that have the same purpose and behaviour. Improve the following code by applying the Phantom type parameters pattern. Check that the signature for the following verifySignature function is produced by the PublicKey, derived for the corresponding PrivateKey that signed the same type of data. There is a nice trick that combines the Maybe data type, do-notation and MonadFail typeclass that allows writing such code. Or if all your verification checks are independent of each other, you can use the Validation data type to output all errors that were fired along the way. However, when using more detailed error-reporting, you won’t be able to make use of this MonadFail trick, because both Either and Validation don’t implement the MonadFail instance, so the manual pattern-matching or other type of handling is required. Interestingly, unlike Either-like data types, the MonadFail instance for lists in Haskell has a similar power to Maybe’s. Implement the following functions applying the MonadFail sugar pattern. Implement the following functions applying the MonadFail sugar pattern. Implement the following functions applying the MonadFail sugar pattern. Implement the following functions applying the MonadFail sugar pattern. Assigning a more general type to a function reduces the chances of writing an incorrect implementation or providing incorrect inputs. To use the same function on values of different types. Haskell has the Polymorphism feature and allows writing and using polymorphic data types and functions. A polymorphic function can be defined only once for some general types, and then it can be called with arguments of different types, satisfying the type signature. If a monomorphic function has type Int -> Int then it can do many different things and you can’t really guess its behaviour by only looking at its type. However, if the function’s type is general as a -> a then this function can do only one thing and in that case, its name doesn’t matter, you already know what it does. 📚 Exercise: Do you see what the function with the type a -> a does? Let’s say we want to implement a function that compares two pairs of Ints and something else by the first element of a pair: Now it is much harder to implement this function in the wrong way and its type signature also tells us much more about its behaviour! It is often in programs, that you use enumeration types (data types with several constructors without any fields). It’s important that you pattern match on all cases of enumeration when possible to avoid having partial functions. You can implement helper functions that will parse your enumeration types from text back to safe Haskell data types. Implement a function that will take a string of two space-separate words — colour and fruit name — and return them as data types. Moving recursion over data types into the separate function. Quite often Haskell developers end-up writing functions that recursively do some actions on different data types: lists, trees, numeric accumulators, etc. You can do this only once, and the recursive go function will work only with valid data. Likewise, you can apply Haskell-specific patterns successfully outside of Haskell.This blog post contains a structured collection of some programming mini-patterns in Haskell with the detailed description and examples, some small “quality of life” improvements that would help everyone on their developer journey.📚 In this article, each pattern contains at least one task with the hidden solution, so you can test your understanding of a pattern by solving the proposed task.NewtypeLightweight data wrapper.When using the same primitive type (Int, Text, etc.) to represent semantically different entities (name, title, description, etc.).One of the most common and useful Haskell features is newtype. newtype is an ordinary data type with the name and a constructor. However, you can define a data type as newtype instead of data only if it has exactly one constructor with exactly one field.It is extremely easy to define a newtype in Haskell as no extra effort is required from the user compared to the data type declaration. For example, the following are the valid newtype definitions:But you can’t declare the following data types as a newtype:newtypes have a lot of great benefits but I’m going to focus only on the code maintainability in this section.You can think of a newtype as a lightweight data type. So, newtype is a zero-cost abstraction.Notwithstanding from the compiler point of view, different newtypes are different data types. And since they don’t have any runtime overhead compared to the ordinary data types, you can provide a safer interface to your API without sacrificing performance.Shall we look at the example now. Since both password and hash are merely textual data, we could write the following function:The problem with this function is that you can easily pass arguments in a different order and get faulty results. This approach is error-prone.But if you define the following newtypes instead:You can implement a more type-safe version of the validateHash function, and additionally this improves the code readability:Now, validateHash hash password is a compile-time error, and it makes it impossible to confuse the order of arguments.Another popular way of increasing code readability without sacrificing performance is introducing a new alias (which is simply another name for a data type) by using the type keyword. And if you don’t help your compiler, the compiler won’t be there for you when you need it.For example, the following code is flawed:Of course, the above type signature is better than Int -> Int -> Int, but the compiler sees it exactly like this. So, you still can write calculateDamage monsterDefense playerAttack and get a runtime bug.The approach of using types instead of newtype can bring even more damage when you have a lot of similar data types. Below you can see a code sample from one of the Haskell libraries:The library safety can be improved by replacing all these type aliases with newtypes, and it can even help to discover some bugs that happen due to passing arguments in the wrong order.Moreover, the newtype approach is more flexible since you can provide your custom instances or restrict some instances allowing you to create values in an unsafe way.The cost of using newtype is small — you only need to wrap and unwrap it when necessary. But the benefits hugely outweigh this small price.Improve the following code by applying the Newtype pattern.Notice how the implementation of the hitPlayer function hasn’t changed at all. However, if you try to swap some of the arguments in different functions now, the compiler will prevent you from accidentally committing an error.Smart constructorProviding idiomatic ways for constructing values.Once you have a newtype, first you need to create its value to work with it. Usually you would create a value of a data type by using its constructor. It can be understood better by looking at the implementation of such approach based on the Password data type:In this module, we want to reject empty passwords. But we need a way to deconstruct a value of type Password hence the Password (unPassword) line in the export list.👩‍🔬 It is important to hide the constructor to forbid value coercion.Even if you don’t allow creating unvalidated passwords, you may need to create passwords in your test-suite without extra hassle. Here we would like to add only a short overview with a small example.If you ever find yourself writing code like this:It is a sign that you are following the boolean blindness anti-pattern and it is time to refactor your code immediately.The key issue here is that by calling a function that returns Bool you lose the information about earlier performed validation. Instead, you can keep this information by explicitly pattern-matching on the validation or result.📚 Exercise: Try refactoring the above code without using isNothing and fromJust functions. Bonus points for using Maybe as a Monad.Returning to our previous example with the validateHash function:You still can forget to call this validation function in the application code and allow users to work with invalid passwords.The above code has two problems:One more type-safe approach (but also a bit more heavyweight solution) would be to return some sort of witness for the fact that a password was validated, and then require this witness in the future functions.To implement this solution, first, we need to change the type of validateHash:Now we change the type of getUserPage to take UserAuth as a required parameter:Since UserAuth can be created only with validateHash, the only way to return a user page is by performing password validation:You can see how we made the code safer and more robust by a small change. For example, the following blog post describes implementation of a similar problem using more advanced Haskell features:Improve the following code by applying the Evidence pattern.Make illegal states unrepresentableData types precisely describe the domain allowing to create all valid values and making it impossible to construct invalid values.This pattern is closely related to smart constructor and evidence patterns, and they can and should be used together.The make illegal states unrepresentable motto is well-known in the FP community. Functional Programming features such as Algebraic Data Types (ADT), parametric polymorphism and others allow describing the shape of the valid data more precisely to the extent that it is impossible to construct any invalid values.To give a simple example of this idea, consider a function that takes two optional values and does something with those values, but only when both values are present. The function assumes that you won’t pass only a single value without the second element, so it doesn’t bother to handle such cases.The type of handleTwoOptionals allows passing Just a and Nothing :: Maybe b, but in reality the function doesn’t process such a combination. You can notice that it is straightforward to fix this particular problem by changing types barely: instead of passing two Maybes separately, you need to pass Maybe of a pair.With this slight change we made it impossible to specify only a single value as Nothing. Note, that it could benefit from the usage of the evidence pattern to bear in mind this fact.Again, it is pretty easy to fix the problem by changing the type of processTwoLists:Instead of passing two lists and expecting them to have the same size, the function simply takes a list of pairs, so it has the same number of as and bs.And one more example. Both setting configurations can be optional, but at least one of the settings parts should be specified.You can start approaching this problem by writing the following code:But the above function has the same problem: the data type makes it possible to specify values that shouldn’t happen in real life. To fix this problem, we need to change the shape our Settings data type in the following way by using sum types:Now, even developers unfamiliar with the codebase won’t be able to create invalid settings. The shape of our data precisely describes all valid states in our program.Generally, by pushing requirements for our data types upstream, we can implement more correct code and make the life of our API users easier, because they can’t shoot themselves in the foot by providing illegal values.However, it is not always possible to easily make all invalid states unrepresentable. But to be exactly precise in our data description, we probably need to create another enumeration type specifying only valid combinations of Answers. In this case, it is not feasible to describe only valid states with a data type, so we don’t bother doing this.Implement the following function by applying the make illegal states unrepresentable pattern.Hint: Use the NonEmpty list.Improve the following code by applying the make illegal states unrepresentable pattern.Phantom type parametersAdditional type-level information available during compile-time by introducing extra type variables.Sometimes you can improve benefits gained with the usage of newtypes even further by using “phantom type variables” — type variables that are not used after the “=” sign in the data type declaration. Instead of writing:or more verboseyou can have one newtype to represent that all:This approach allows you to avoid creating multiple data types that have the same purpose and behaviour.For example, you can have a lot of instances for your newtypes and don’t want to repeat them for UserId, AdminId, CommentId, MessageId, etc. Adding a phantom type parameter to a single data type is a minor yet powerful change to your data types that imposes an extra layer of type-safety to your code.Going back again to our favourite example with passwords: you can have multiple entities that can log into your application (users, administrators, third-parties, etc.). You could use phantom type variables here as well in order to track sign-in information on the type-level and bind the password with its hash using the type-level tag:Now it is no longer possible to validate the password of a user with the hash of an admin.📚 Exercise: The mkPasswordhash function takes a password and maybe returns password hash. 😉Improve the following code by applying the Phantom type parameters pattern. Don’t worry about function implementations, they are not important in this task.Check that the signature for the following verifySignature function is produced by the PublicKey, derived for the corresponding PrivateKey that signed the same type of data. The implementation guarantees that you cannot verify signature for a different type than the signature was created initially.MonadFail sugarElegant syntax for pattern-matching on nested or multiple different parts of the data.When time comes to extract deeply-nested fields of a nested data structure, or to perform multiple validations in a single block, you start thinking on the neat way to do it at the best cost. You obviously fancy to avoid using partial functions because they can sometimes hide unhandled cases and fail at runtime unexpectedly.There is a nice trick that combines the Maybe data type, do-notation and MonadFail typeclass that allows writing such code. Let’s explore the following example.When you write a function like this:and if you pass a number like 37 to this function, it will surely fail.However, when you pattern-match on any value on the left side of <- inside do-block, a slightly different set of rules is used for handling non-covered patterns. Such verification can be easily written using the Maybe type and its MonadFail instance:You can see that the code is clean, it does exactly what we expect with a minimal syntactic overhead, and it is also safe as a bonus.📚 Exercise: Desugar the above code manually.In some cases, when performing multiple checks, it is crucial to know which one failed first in order to run the corresponding action next or provide a better error message for users. Or if all your verification checks are independent of each other, you can use the Validation data type to output all errors that were fired along the way. The approach with Either will stop on the first error, when the Validation approach will perform all checks anyways.However, when using more detailed error-reporting, you won’t be able to make use of this MonadFail trick, because both Either and Validation don’t implement the MonadFail instance, so the manual pattern-matching or other type of handling is required.Interestingly, unlike Either-like data types, the MonadFail instance for lists in Haskell has a similar power to Maybe’s. The description is wordy, but the code for this task with the usage of list comprehension is much more elegant:If you attempt to implement such a function by using manual pattern-matching, it would look a bit less cleaner.📚 Exercise: Try implementing the keepOnlySameRights functions without list comprehensions and the MonadFail instance for list.Implement the following functions applying the MonadFail sugar pattern.Implement the following functions applying the MonadFail sugar pattern.Implement the following functions applying the MonadFail sugar pattern.Implement the following functions applying the MonadFail sugar pattern.PolymorphisationAssigning a more general type to a function reduces the chances of writing an incorrect implementation or providing incorrect inputs.Haskell has the Polymorphism feature and allows writing and using polymorphic data types and functions. A polymorphic function can be defined only once for some general types, and then it can be called with arguments of different types, satisfying the type signature.For example, the following functioncan have types, depending on arguments, either firstArgs :: Int -> String -> Int or firstArg :: Maybe s -> [a] -> Maybe s but not firstArg :: Int -> Double -> Double.If a monomorphic function has type Int -> Int then it can do many different things and you can’t really guess its behaviour by only looking at its type. You can guess by the function name, but naming is hard and you still need to check the documentation and probably even source code.However, if the function’s type is general as a -> a then this function can do only one thing and in that case, its name doesn’t matter, you already know what it does.📚 Exercise: Do you see what the function with the type a -> a does?The thought that a more general function is less powerful is counter-intuitive. The following elaborate example demonstrates key features of the polymorphisation approach.Let’s say we want to implement a function that compares two pairs of Ints and something else by the first element of a pair:This type is fine, however, it’s possible to implement not exactly what we wanted but still satisfy this type signature. So instead of using Int we can use some polymorphic variable:Though, since the function produces Ordering, you still can simply ignore all arguments and return EQ in all cases. The following two implementations are valid:When you are working only on this function, you may notice such logic errors during the development. Unfortunately, this type signature has several problems:A better type signature would be:Now it is much harder to implement this function in the wrong way and its type signature also tells us much more about its behaviour!📚 Exercise: Implement partitionWith.👩‍🔬 It is still possible to implement partition in a wrong way by not adding elements to the result list. Instead of filtering elements by predicate and using unsafe conversion functions, you can use mapMaybe and make illegal states unrepresentable.Look at the following function:It can be written in a slightly different way, that also allows extending filtering rules easily:Improve the following functions by applying the Polymorphisation pattern.Bidirectional parsingMatching only a limited set with exhaustiveness checking and inversing matching function automatically.It is often in programs, that you use enumeration types (data types with several constructors without any fields). This is problematic because when handling strings via pattern-matching, you are physically unable to handle all cases because there is an infinite number of them.Consider the following example:It’s important that you pattern match on all cases of enumeration when possible to avoid having partial functions.If you add a new colour constructor, for example Orange, the compiler will warn you to update the showColour function. You can implement helper functions that will parse your enumeration types from text back to safe Haskell data types. One of such functions is inverseMap from relude:📚 Exercise: Implement the inverseMap function.Using this function, you can rewrite parseColour in a simpler and more safe way:Even if you are not using the showColour function, this approach still can be a better solution, since you can handle cases exhaustively and get a parsing function for free.You can find more details about this approach in the Haddock documentation for the inverseMap function.Implement a function that will take a string of two space-separate words — colour and fruit name — and return them as data types.Possible colours: red, green, yellow, blue. Possible fruits: apple, orange, lemon, blueberry.Example:Recursive goMoving recursion over data types into the separate function.🙅 Disclaimer This pattern is not about recursive functions in the Go programming language.Quite often Haskell developers end-up writing functions that recursively do some actions on different data types: lists, trees, numeric accumulators, etc. In our case the stop signals are:The step for this function is to examine the tail of the list (throwing away the first element) and decreasing the position number by one (for each thrown element).Haskell implementation may look like this:However, there is another way to write the above function. Here we are going to use the recursive go pattern – the pattern when instead of using the function itself for the recursion, you create the internal so-called go function that will do that.The idea here is to run the go function with the empty “accumulator” and the starting list, and the go function should recursively update the accumulator and walk the whole list. This accumulator would be compared to the input position (in contrast to the comparison to 0 when you decrease the original value in the previous case).You can code this idea this way:The way the go pattern is implemented also is more flexible in the refactoring and “requirements”-change. You can do this only once, and the recursive go function will work only with valid data.📚 Exercise: Can you make these at/atGo functions more efficient on the corner cases?This pattern could be especially handy when you need to do additional actions on the input data after you get the result of the recursive calculations. Or if you want to avoid passing arguments that never change during the recursive traverse of your data structure (e.g. map, filter, foldr, etc.).Improve the following code by applying the Recursive go pattern.Write the ordNub function with the Recursive go pattern. The function should return all unique elements from a list in the order of their appearance.In this blog post we covered some small techniques that increase code maintainability, readability and reusability.

As said here by Kowainik ? Dmitrii Kovanikov <> Veronika Romashkina