问题描述
tl;博士
鉴于 ixmap
的签名和 contramap
的签名之间的相似性,我想了解 Array i e
的第一个类型参数是否是逆变函子,或者至少,从范畴论的角度来看,这两件事是如何相互关联的。
更长的润滑
在Chapter 12 from Real World Haskell的末尾,使用了函数ixmap
。
基于其签名
ixmap :: (Ix i,Ix j) => (i,i) -> (i -> j) -> Array j e -> Array i e
我不禁注意到,一旦我们将其部分应用于第一个参数,例如为了简单起见,我们传递给它 (1 :: Int,1 :: Int)
,它的签名变成
ixmap (1 :: Int,1 :: Int) :: Ix j => (Int -> j) -> Array j e -> Array Int e
与contramap
的签名有些相似:
contramap :: (a' -> a) -> f a -> f a'
甚至更多,因为它专门针对 Op
:
contramap :: (a' -> a0) -> Op a a0 -> Op a a'
毕竟,我认为 Array j e
类型可以看作是将 j
类型的子集映射到 e
类型的函数的类型,有点像 {{1} } 带有“受限”j -> a
。因此,就像 j
是 b -> a
中的 Functor
并且 a
被定义为使其成为 Op a b
中的逆变函子一样,我想我可以类似地定义:
b
并为其编写一个 newtype Array' e i = Array' { arr :: Array i e }
实例:
Contravariant
令我不安的是,我不能真正将部分应用的 instance Contravariant (Array' e) where
contramap f a = undefined -- ???
用于 ixmap
,因为 (1) 我所做的部分应用它?并且 (2) 这样做会阻止 contramap
类型(例如,在我的示例中为 i
)。
我什至想不出让 Int
从另外两个参数 contrampa
和 (i,i)
中重试所需的 f :: (i -> j)
类型对象的方法,因为我没有从 a :: Array j e
到 j
的函数。
解决方法
Array
是一个 profunctor,从索引重映射函数类别到普通 Hask 类别(以 Haskell 函数为态射的无约束 Haskell 类型)。
有一个相当普遍的 class for profunctors Hask → Hask,但它无法表达 Ix
约束。不过,这很容易在 constrained-categories 框架中表达†:
class (Category r,Category t) => Profunctor p r t where
dimap :: (Object r a,Object r b,Object t c,Object t d)
=> r a b -> t c d -> p b c -> p a d
现在,要实际将其与 Array
一起使用,我们需要将允许的索引范围提升到类型级别。 IE。而不是使用 Int
作为索引类型 - 这有点不安全,因为它允许索引超出范围......我们显然不能在类别理论设置中使用它! – 我们只使用一种具有允许范围的类型。而不是实际将其黑入 Array
,让我使用 Vector
(它根本不打扰提供不同的索引类型)作为低-级别表示:
{-# LANGUAGE DataKinds,TypeFamilies,AllowAmbiguousTypes,TypeApplications,ScopedTypeVariables,UnicodeSyntax #-}
import GHC.TypeLits (Nat,natVal)
import Data.Vector (Vector)
import qualified Data.Vector as V
newtype Range (lb :: Nat) (ub :: Nat)
= Range { getIndexInRange :: Int -- from 0 to ub-lb-1
}
newtype SafeArray i a = SafeArray {
getFlattenedArray :: Vector a -- length must equal `rangeLength @i`
}
class ToLinearIndex r where
rangeLength :: Int
toLinearIndex :: r -> Int
instance ∀ lb ub . ToLinearIndex (Range lb ub) where
rangeLength = fromInteger $ natVal @rb [] - natVal @lb []
toLinearIndex = getIndexInRange
instance ∀ rx ry . (ToLinearIndex rx,ToLinearIndex ry)
=> ToLinearIndex (rx,ry) where
rangeLength = rangeLength @rx * rangeLength @ry
toLinearIndex (ix,iy)
= toLinearIndex ix + rangeLength @rx * toLinearIndex iy
(!) :: ToLinearIndex i => SafeArray i a -> i -> a
SafeArray v ! i = V.unsafeIndex v $ toLinearIndex i
newtype IxMapFn r s = IxMapFn {
getIxMapFn :: Int -> Int -- input and output must be <rangeLength
-- of `r` and `s`,respectively
}
instance Category IxMapFn where
type Object IxMapFn i = ToLinearIndex i
id = IxMapFn id
IxMapFn f . IxMapFn g = IxMapFn $ f . g
saDiMap :: ∀ r s a b . (ToLinearIndex r,ToLinearIndex s)
=> IxMapFn s r -> (a -> b) -> SafeArray r a -> SafeArray s b
saDiMap (IxMapFn f) g (SafeArray v)
= SafeArray . V.generate (rangeLength @s)
$ g . V.unsafeIndex v . f
instance Profunctor SafeArray IxMapFn (->) where
dimap = saDimMap
†我从来没有考虑给
constrained-categories
添加一个 profunctor 类,主要是因为我认为 profunctor 在 Haskell 中有点被滥用了:通常当人们使用内profunctors,他们实际上想表达的只是一个类别/Arrow
。