Parallel typeclass for Haskell

栏目: IT技术 · 发布时间: 4年前

内容简介:As I’m preparing a talk about refinement types I will be giving this Thursday at theIn the following sections, I will be providing examples and use cases for this typeclass to showcase why it would be great to have it in Haskell. Oh, yes… I love refinement

As I’m preparing a talk about refinement types I will be giving this Thursday at the Functional Tricity Meetup , and I’ve recently given a similar talk using the Scala language as well, I realized there is a missing typeclass in Haskell.

In the following sections, I will be providing examples and use cases for this typeclass to showcase why it would be great to have it in Haskell. Oh, yes… I love refinement types as well!

In Haskell, we have the refined library and other more complex tools such as Liquid Haskell .

Refinement types

Refinement types give us the ability to define validation rules, or more commonly called predicates , at the type level. This means we get compile-time validation whenever the values are known at compile-time.

Say we have the following predicates and datatype:

import Refined

type Age  = Refine (GreaterThan 17) Int
type Name = Refine NonEmpty Text

data Person = Person
  { personAge :: Age
  , personName :: Name
  } deriving Show

We can validate the creation of Person at compile-time using Template Haskell:

me :: Person
me = Person $$(refineTH 32) $$(refineTH "Gabriel")

If the age was a number under 18, or the name was an empty string, then our program wouldn’t compile. Isn’t that cool?

Though, most of the time, we need to validate incoming data from external services, meaning runtime validation . Refined gives us a bunch of useful functions to achieve this, effectively replacing smart constructors . The most common one is defined as follows:

refine :: Predicate p x => x -> Either RefineException (Refined p x)

We can then use this function to validate our input data.

mkPerson :: Int -> Text -> Either RefineException Person
mkPerson a n = do
  age  <- refine a
  name <- refine n
  return $ Person age name

However, the program above will short-circuit on the first error, as any other Monad will do. It would be nice if we could validate all our inputs in parallel and accumulates errors, wouldn’t it?

We can achieve this by converting our Either values given by refine a into Validation , use Applicative functions to compose the different parts, and finally converting back to Either .

import Data.Validation

mkPerson :: Int -> Text -> Either RefineException Person
mkPerson a n = toEither $ Person
  <$> fromEither (refine a)
  <*> fromEither (refine n)

As we can see, it is a bit clunky, and this is a very repetitive task, which will only increase the amount of boilerplate in our codebase.

This seems to be the status quo around validation in Haskell nowadays, and it was the same in Scala. So it’s kind of hard to realize we are missing what we don’t know: the Parallel typeclass. I didn’t know it was such a game changer until I started using it everywhere.

This is exactly what this typeclass does for us in other languages, via its helpful functions and instances. Unfortunately, it doesn’t exist in Haskell, as far as I know… until now!

Parallel typeclass

Let me introduce you to the Parallel typeclass, already present in PureScript and Scala :

import Control.Natural ((:~>))

class (Monad m, Applicative f) => Parallel f m | m -> f, f -> m where
  parallel :: m :~> f
  sequential :: f :~> m

It defines a relationship between a Monad that can also be an Applicative with “parallely” behavior. That is, an Applicative instance that wouln’t pass the monadic laws.

The most common relationship is the one given by Either and Validation . These two types are isomorphic, with the difference being that Validation has an Applicative instance that accumulate errors instead of short-circuiting on the first error.

So we can represent this relationship via natural transformation in a Parallel instance:

instance Semigroup e => Parallel (Validation e) (Either e) where
  parallel   = NT fromEither
  sequential = NT toEither

In the same way, we can represent the relationship between [] and ZipList :

instance Parallel ZipList [] where
  parallel   = NT ZipList
  sequential = NT getZipList

Now, all this ceremony only becomes useful if we define some functions based on Parallel . One of the most common ones is parMapN (or parMap2 in this case, but ideally, it should be abstracted over its arity).

parMapN
  :: (Applicative f, Monad m, Parallel f m)
  => m a0
  -> m a1
  -> (a0 -> a1 -> a)
  -> m a
parMapN ma0 ma1 f = unwrapNT sequential
  (f <$> unwrapNT parallel ma0 <*> unwrapNT parallel ma1)

Before we get to see how we can leverage this function with refinement types and data validation, we will define a type alias for our effect type and a function ref , which will convert RefineException s into a [Text] , since our error type needs to be a Semigroup .

import Control.Arrow (left)
import Data.Text     (pack)
import Refined

type Eff a = Either [Text] a

ref :: Predicate p x => x -> Eff (Refined p x)
ref x = left (\e -> [pack $ show e]) (refine x)

In the example below, we can appreciate how this function can be used to create a Person instance with validated input data (it’s a breeze):

mkPerson :: Int -> Text -> Eff Person
mkPerson a n = parMapN (ref a) (ref n) Person

Our mkPerson is now validating all our inputs in parallel via an implicit round-trip Either / Validation given by our Parallel instance.

We can also use parMapN to use a different Applicative instance for lists without manually wrapping / unwrapping ZipList s.

n1 = [1..5]
n2 = [6..10]

n3 :: [Int]
n3 = (+) <$> n1 <*> n2

n4 :: [Int]
n4 = parMapN n1 n2 (+)

Without Parallel ’s simplicity, it would look as follows:

n4 :: [Int]
n4 = getZipList $ (+) <$> ZipList n1 <*> ZipList n2

For convenience, here’s another function we can define in terms of parMapN :

parTupled
  :: (Applicative f, Monad m, Parallel f m)
  => m a0
  -> m a1
  -> m (a0, a1)
parTupled ma0 ma1 = parMapN ma0 ma1 (,)

In Scala, there’s also an instance for IO and IO.Par , a newtype that provides a different Applicative instance, which allows us to use functions such as parMapN with IO computations to run them in parallel!

And this is only the beginning… There are so many other useful functions we could define!

For now, the code is presented in this Github repository together with some other examples. Should there be enough interest, I might polish it and ship it as a library.

Let me know your thoughts!

Gabriel.


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

阿里巴巴Java开发手册

阿里巴巴Java开发手册

杨冠宝 / 电子工业出版社 / 2018-1 / 35

《阿里巴巴Java开发手册》的愿景是码出高效,码出质量。它结合作者的开发经验和架构历程,提炼阿里巴巴集团技术团队的集体编程经验和软件设计智慧,浓缩成为立体的编程规范和最佳实践。众所周知,现代软件行业的高速发展对开发者的综合素质要求越来越高,因为不仅是编程相关的知识点,其他维度的知识点也会影响软件的最终交付质量,比如,数据库的表结构和索引设计缺陷可能带来软件的架构缺陷或性能风险;单元测试的失位导致集......一起来看看 《阿里巴巴Java开发手册》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具