Can I send multiple to Haskell with pattern matching in class classes?

This is a question about several mailings in Haskell.

Below I use the term "compatible with [type class]" means "has a type that is an instance of [type class]" because type classes are often interfaces, so it intuitively understands a specific thing, such as the actual value of Int as " compatible "with the interface / type class because of its type, which implements everything that is necessary in order to belong to this interface / type class.

Let's look at an example of how to make one exponential function that will work regardless of whether it is called with arguments Floating , Num , Integral or something else, and works using type classes that implement argument types to select a pre-existing exponential function to call.

The function (^) has type (^) :: (Integral b, Num a) => a -> b -> a , and the function (**) has type (**) :: Floating a => a -> a -> a .

Suppose I want to create a function my_pow that takes the first argument Num and the second argument Num .

If both arguments match Floating , then it will call (**) ; if the second argument just matches Integral , it will call (^) ; and any other case will give a pattern matching error.

My naive first attempt was to look at type classes, such as value constructors, and try to map the image in a function definition:

 my_pow :: (Num a, Num b) => a -> b -> a my_pow (Floating x) (Floating y) = x ** y my_pow x (Integral y) = x ^ y 

but this gives errors:

 tmp.hs:25:6: Not in scope: data constructor `Floating' tmp.hs:25:19: Not in scope: data constructor `Floating' tmp.hs:26:8: Not in scope: data constructor `Integral' 

probably means that I cannot consider class classes as value constructors, which is not surprising.

But then Googling around for how to match patterns with specific class properties of the class of arguments, which are more specific than class class restrictions in the function definition, did not give clear answers.

What is the preferred way to create this kind of polymorphism - it is effective to send a template when a function has relaxed class type restrictions in general, but then it is determined by matching patterns with more specific class type restrictions for any of the cases, sending other functions.

+6
source share
2 answers

The usual way to "match patterns" by type in the way you describe is with type instances. With specific types, this is easy to use with MultiParamTypeClasses ; this is how Haskell implements multiple dispatchers.

 {-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, OverlappingInstances #-} module SO26303353 where class (Num a, Num b) => Power ab where my_pow :: a -> b -> a instance Power Double Double where my_pow = (**) instance Num a => Power a Integer where my_pow = (^) 

This works great. This is more or less idiomatic Haskell, except that (**) and (^) are different operations, and some people may object to blurring the difference.

However, you are asking for something more complex. You want to send multiple times not only by type, but also by type class. This is a much different and more powerful thing. In particular, it will work for all types that Floating or Intergral instances can have, even types that have not yet been written! Here's how it would be written perfectly:

 instance (Floating a) => Power aa where my_pow = (**) instance (Num a, Integral b) => Power ab where my_pow = (^) 

This does not work , however, since the constraint resolver does not back down and does not take into account the constraints of the instance when selecting the instance. Thus, my_pow does not work, for example, with two Int s:

 ghci> :t my_pow :: Int -> Int -> Int No instance for (Floating Int) 

This is because there is a “more specific" Power aa instance, since both types are equal. The GHC then imposes a Floating constraint on a and barfs when it cannot satisfy it. Then it does not back down and does not try to execute an instance of Power ab .

It may or may not be possible to get around the restriction using extended functions of a system like, but I don’t think you could make a replacement for a replacement of both (**) and (^) in the current Haskell.

Edit: general comments

(Please note that we are distracting from the Q & A format here.)

Rereading my question and commentary, I notice that you use the term “sending” in a way that I am not familiar with. Quick Google opens up dual-post articles and a visitor design template . Where are you from? They are a bit like what you are trying to do - write a function that does completely different things based on the types of its arguments. I want to add a few things to this answer, which can help hone your sense of idiomatic Haskell. (Or maybe just disconnected incoherent.)

Haskell usually ignores the idea of ​​"type runtime". Even in @Cirdec’s more complex answer, all types are statically known "at compile time". (Using REPL, ghci does not change anything except that the “compilation time” becomes a bit foggy.) In fact, the intuitions about what happens “at runtime” often differ in Haskell than other languages, not least because GHC performs aggressive optimization.

Idiomatic Haskell is based on parametric polymorphism ; a function of type replicate :: Int -> a -> [a] works exactly the same for any type a . As a result, we know a lot about what replicate does without having to look at its implementation. This relationship is really helpful, and it deeply infects the brain of Haskell programmers. You will notice that I and many other Haskell programmers are crazy about type annotations, especially on a forum like this. Static types are very significant. (Keyword: free theorems.) (This does not apply directly to your question.)

Haskell uses type classes that allow ad hoc polymorphism . In my opinion, "ad hoc" refers to the fact that the implementation of a function can be different for different types. This, of course, is important for numeric types and has been applied for many years in countless ways. But it is important to understand that it is still statically typed, even with class types. To actually evaluate any type-type function - to get a value from it, you need to select a specific type at the end. (With numeric types, default rules often choose it for you.) Of course, you can combine things to create another polymorphic function (or value).

Historically, class classes were considered strictly a mechanism of function overloading in the sense of the same name for several different functions. In other words, instead of addInt :: Int -> Int -> Int , addFloat :: Float -> Float -> Float , we have one name: (+) :: Num a => a -> a -> a . But this is still basically the same idea: there are a bunch of completely different functions called (+) . (Now we tend to talk about class types in terms of "laws", but that's another topic.) Often there is no literal dispatch with a function of type (+) or even non-primitive functions.

Yes, class types are a bit like interfaces, but don't let OOP thinking creep in too much. If you are writing a function with a type of type Num a => a -> a , expect the only thing you know about a is that it is an instance of Num . You cannot look behind the curtain. (Without cheating. It's complicated). The only way to manipulate values ​​of type a is with fully polymorphic functions and other Num functions. In particular, you cannot determine if a also an instance of any other class.

The various compiler extensions that we played blurred this model a bit, because now we can write, in fact, types of level functions. But do not confuse this with dynamic dispatch.

Oh, by the way, Haskell supports dynamic types. See Data.Dymamic . Honestly, I have never seen much use for it beyond interacting with other languages. (I'm ready to make mistakes.) Typical problems with the visitor pattern can be implemented in other ways.

+9
source

Like Christian Conkle , we can determine if a type is an Integral or Floating instance using more complex system functions. We will try to determine if the second argument has an Integral instance. Along the way, we will use many language extensions and still be a little behind our goal. I will introduce the following language extensions where they are used

 {-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverlappingInstances #-} 

Transforming Integral Context for Input

To begin with, we will make a class that will try to capture information from the context of the type (whether there is an instance of Integral ) and convert it to a type that we can match. This requires the extension FunctionalDependencies to say that flag can be uniquely determined from type a . It also requires MultiParamTypeClasses .

 class IsIntegral a flag | a -> flag 

We will use two types for the flag type to represent when the type is ( HTrue ) or does not have ( HFalse ) an Integral instance. This uses the EmptyDataDecls extension.

 data HTrue data HFalse 

We will provide a default value - if for a there is no IsIntegral instance that causes flag be something other than HFalse , we provide an instance that says it is HFalse . This requires extensions to TypeFamilies , FlexibleInstances and UndecidableInstances .

 instance (flag ~ HFalse) => IsIntegral a flag 

We would like to say that each a with an instance of Integral a has an instance of IsIntegral a HTrue . Unfortunately, if we add an instance of instance (Integral a) => IsIntegral a HTrue , we will be in the same situation that the Christian was talking about. This second instance will be used by preference, and when the Integral constraint is encountered, it will be added to the context without returning. Instead, we will need to list all Integral types. Here we miss our goal. (I skip the base Integral types from System.Posix.Types , since they are not defined identically on all platforms).

 import Data.Int import Data.Word import Foreign.C.Types import Foreign.Ptr instance IsIntegral Int HTrue instance IsIntegral Int8 HTrue instance IsIntegral Int16 HTrue instance IsIntegral Int32 HTrue instance IsIntegral Int64 HTrue instance IsIntegral Integer HTrue instance IsIntegral Word HTrue instance IsIntegral Word8 HTrue instance IsIntegral Word16 HTrue instance IsIntegral Word32 HTrue instance IsIntegral Word64 HTrue instance IsIntegral CUIntMax HTrue instance IsIntegral CIntMax HTrue instance IsIntegral CUIntPtr HTrue instance IsIntegral CIntPtr HTrue instance IsIntegral CSigAtomic HTrue instance IsIntegral CWchar HTrue instance IsIntegral CSize HTrue instance IsIntegral CPtrdiff HTrue instance IsIntegral CULLong HTrue instance IsIntegral CLLong HTrue instance IsIntegral CULong HTrue instance IsIntegral CLong HTrue instance IsIntegral CUInt HTrue instance IsIntegral CInt HTrue instance IsIntegral CUShort HTrue instance IsIntegral CShort HTrue instance IsIntegral CUChar HTrue instance IsIntegral CSChar HTrue instance IsIntegral CChar HTrue instance IsIntegral IntPtr HTrue instance IsIntegral WordPtr HTrue 

Compliance IsIntegral

Our ultimate goal is to provide the appropriate instances for the next class.

 class (Num a, Num b) => Power ab where pow :: a -> b -> a 

We want to map types to choose which code to use. We will create a class with an additional type to hold the flag for whether b Integral type. pow' optional argument allows you to enter type inference to use the correct pow' .

 class (Num a, Num b) => Power' flag ab where pow' :: flag -> a -> b -> a 

Now we will write two instances: one when b is Integral and the other when it is not. If b not Integral , we can provide an instance only when a and b match.

 instance (Num a, Integral b) => Power' HTrue ab where pow' _ = (^) instance (Floating a, a ~ b) => Power' HFalse ab where pow' _ = (**) 

Now that we can determine if there is b Integral with IsIntegral and can provide a Power' instance for this result, we can provide a Power instance, which was our goal. This requires the extension ScopedTypeVariables to get the correct type for pow' optional argument

 instance (IsIntegral b flag, Power' flag ab) => Power ab where pow = pow' (undefined::flag) 

In fact, using these definitions requires an extension to OverlappingInstances .

 main = do print (pow 7 (7 :: Int)) print (pow 8.3 (7 :: Int)) print (pow 1.2 (1.2 :: Double)) print (pow 7 (7 :: Double)) 

You can read another explanation on how to use FunctionalDependencies or TypeFamilies to avoid duplication in overlapping instances in Advanced Overlap . on the HaskellWiki.

+4
source

Source: https://habr.com/ru/post/1208946/


All Articles