Can multiple dispatch be achieved in Haskell with pattern matching on type classes?

前端 未结 2 1884
情话喂你
情话喂你 2021-02-05 18:41

This is a question about multiple dispatch in Haskell.

Below I use the term \"compliant to [type class]\" to mean \"has type which is instance of [type class]\", because

2条回答
  •  失恋的感觉
    2021-02-05 19:06

    Like Christian Conkle hinted at, we can determine if a type has an Integral or Floating instance using more advanced type system features. We will try to determine if the second argument has an Integral instance. Along the way we will use a host of language extensions, and still fall a bit short of our goal. I'll 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 #-}
    

    Convert Integral context to type

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

    class IsIntegral a flag | a -> flag
    

    We'll make two types to use for the flag type to represent when a type does (HTrue) or doesn't (HFalse) have an Integral instance. This uses the EmptyDataDecls extension.

    data HTrue
    data HFalse
    

    We'll provide a default - when there isn't an IsIntegral instance for a that forces flag to be something other than HFalse we provide an instance that says it's HFalse. This requires the TypeFamilies, FlexibleInstances, and UndecidableInstances extensions.

    instance (flag ~ HFalse) => IsIntegral a flag
    

    What we'd really like to do is say that every a with an Integral a instance has an IsIntegral a HTrue instance. Unfortunately, if we add an instance (Integral a) => IsIntegral a HTrue instance we will be in the same situation Christian described. This second instance will be used by preference, and when the Integral constraint is encountered it will be added to the context with no backtracking. Instead we will need to list all the Integral types ourselves. This is where we fall short of our goal. (I'm skipping the base Integral types from System.Posix.Types since they aren't defined equally 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
    

    Matching on IsIntegral

    Our end goal is to be able to provide appropriate instances for the following class

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

    We want to match on types to choose which code to use. We'll make a class with an extra type to hold the flag for whether b is an Integral type. The extra argument to pow' lets type inference choose the correct pow' to use.

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

    Now we'll write two instances, one for when b is Integral and one for when it isn't. When b isn't Integral, we can only provide an instance when a and b are the same.

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

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

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

    Actually using these definitions requires the OverlappingInstances extension.

    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 of how to use FunctionalDependencies or TypeFamilies to avoid overlap in overlapping instances in the Advanced Overlap article on HaskellWiki.

提交回复
热议问题