如何对类型类实例施加类型约束?

thi*_*ndy 1 haskell

我正在通过阅读Learn a Haskell for Great Good 来学习 Haskell!在“创建我们自己的类型和类型类”部分的末尾,YesNo定义了一个类来模拟 javascript 等语言的真实性:

class YesNo a where  
    yesno :: a -> Bool 

instance YesNo Int where  
    yesno 0 = False  
    yesno _ = True  

(etc.)
Run Code Online (Sandbox Code Playgroud)

在阅读参考文献之前,我试图自己充实这些实例作为练习,并认为我可以聪明地为所有Num类型定义它:

instance (Num a) => YesNo a where
  yesno 0 = False
  yesno _ = True
Run Code Online (Sandbox Code Playgroud)

我将跳过这需要什么FlexibleInstances,我想我已经在文档和这个答案之间理解了。一旦打开,编译器就会抱怨“约束‘Num a’不小于实例头‘YesNo a’”。这个问题的答案很好地解释了这意味着什么。使用newtype那里提供的解决方案,我想出了类似的东西

newtype TruthyNum a = TruthyNum a

instance (Num a, Eq a) => YesNo (TruthyNum a) where
  yesno (TruthyNum 0) = False
  yesno _ = True
Run Code Online (Sandbox Code Playgroud)

但现在我不得不说egyesno $ TruthyNum 0而不是yesno 0

这感觉不对。难道真的没有办法在不为每个类型编写一个实例的情况下yesno干净地表达类型吗?Num或者,退一步说,一个经验丰富的 Haskell 黑客如何以“定义一个以[选择你的脚本语言]的方式实现真实性的类型类”为前提?

Ice*_*ack 5

非常好的问题!我会newtype像你一样定义 a 。我不会直接使用它,而是通过它派生。

{-# Language DerivingVia              #-}
{-# Language StandaloneDeriving       #-}
{-# Language StandaloneKindSignatures #-}

import Data.Kind (Type, Constraint)

type  YesNo :: Type -> Constraint
class YesNo a where
  yesno :: a -> Bool

type    TruthyNum :: Type -> Type
newtype TruthyNum a = TruthyNum a

instance (Num a, Eq a) => YesNo (TruthyNum a) where
  yesno (TruthyNum 0) = False
  yesno _             = True

-- standalone deriving, is used when deriving an instance
-- outside of the data declaration
deriving via TruthyNum Int
  instance YesNo Int
deriving via TruthyNum Integer
  instance YesNo Integer
deriving via TruthyNum Float
  instance YesNo Float
Run Code Online (Sandbox Code Playgroud)

Applicative举重是另一种以这种方式重叠的行为。鉴于Applicative f你可以像这样提升代数

  • Semigroup a, Monoid a,Num a

进入

  • Semigroup (f a), Monoid (f a),Num (f a)
instance (Applicative f, Num a) => Num (f a) where
  (+)         = liftA2 (+)
  (-)         = liftA2 (-)
  (*)         = liftA2 (*)
  negate      = liftA  . negate
  abs         = liftA  . abs
  signum      = liftA  . signum
  fromInteger = liftA0 . fromInteger where liftA0 = pure
Run Code Online (Sandbox Code Playgroud)

我们不编写重叠的实例,而是创建newtypeAp f a的实例。

type    Ap :: (k -> Type) -> (k -> Type)
newtype Ap f a = Ap (f a)
  deriving newtype (Functor, Applicative, ..)

instance (Applicative f, Num a) => Num (Ap @Type f a) where
  (+)         = liftA2 (+)
  (-)         = liftA2 (-)
  (*)         = liftA2 (*)
  negate      = liftA  . negate
  abs         = liftA  . abs
  signum      = liftA  . signum
  fromInteger = liftA0 . fromInteger where liftA0 = pure
Run Code Online (Sandbox Code Playgroud)

这是一个蹦床示例,它派生依赖于先前派生的实例。该数据类型是一个“3D 矢量”,但它只是一个具有 3 个相同类型参数的数据类型。

  1. 内置的等式推导和通用表示V3
  2. Generically1(a )使用 1 中的通用表示法newtype导出。Applicative
  3. ApNum从 2推导出来Applicative
  4. TruthyNumYesNo使用等式和来自 3 的实例进行推导Num

我们Applicative得出的涉及提升pure a = V3 a a a,因此当我们写作时,0 :: V3 Int我们实际上是指V3 0 0 0 :: V3 Int

这意味着您的YesNo实例是通过与(/= V3 0 0 0). V3因此,当值为 时,我们将其视为“假” 0

-- >> 0 :: V3 Int
-- V3 0 0 0
-- >> yesno (V3 0 0 0)
-- False
-- >> yesno (V3 0 0 2)
-- True
data V3 a = V3 a a a
  deriving
  stock (Eq, Show, Generic1)

  deriving (Functor, Applicative)
  via Generically1 V3

  deriving (Semigroup, Monoid, Num)
  via Ap V3 a

  deriving YesNo
  via TruthyNum (V3 a)

Run Code Online (Sandbox Code Playgroud)