混合类型模式匹配和记录语法

huangapple go评论57阅读模式
英文:

Mixing type pattern matching and record syntax

问题

让我们假设我有以下的函数 f 的(虚构的示例):

data T where
  T :: (Typeable a, Integral a) => { first :: a, second :: a } -> T

printType :: forall a. Typeable a => String
printType = show $ typeRep (Proxy :: Proxy a)

f :: T -> String
f (T first second) = show (toInteger first, toInteger second)

我可以选择使用记录语法:

f (T {first, second}) = show (toInteger first, toInteger second)

但假设我想要打印类型。然后,我可以使用类型应用:

f (T @a first second) = printType @a ++ ": " ++ show (toInteger first, toInteger second)

但是,当我将类型应用和记录语法结合在一起时,它无法编译通过:

f (T @a {first, second}) = printType @a ++ ": " ++ show (toInteger first, toInteger second)

是否有任何方法可以在仍然匹配类型的同时使用记录语法?或者是否有一个不太丑陋的语法技巧可以用来解决这个问题?

英文:

Lets say I have the following (contrived example) for the function f:

data T where
  T :: (Typeable a, Integral a) => { first :: a, second :: a } -> T

printType :: forall a. Typeable a => String
printType = show $ typeRep (Proxy :: Proxy a)

f :: T -> String
f (T first second) = show (toInteger first, toInteger second)

I can alternatively use record syntax:

f (T {first, second}) = show (toInteger first, toInteger second)

But lets say I wanted to print the type. Then I use a type application:

f (T @a first second) = printType @a ++ ": " ++ show (toInteger first, toInteger second)

But when I put the type application and record syntax together, it fails to compile:

f (T @a {first, second}) = printType @a ++ ": " ++ show (toInteger first, toInteger second)

Is there any way I can use record syntax whilst still also matching on the type? Or alternatively is there a not too ugly syntax hack I can use to work around this?

答案1

得分: 2

抱歉,代码部分不进行翻译,以下是已翻译的内容:

Well, that is unfortunate. Here are some ideas. First, you could match twice.

f x = case x of
    T @a _ _ -> case x of
        T {first, second} -> ...

-- OR, slightly shorter:
f x@(T @a _ _) = case x of
    T {first, second} -> ...

Unfortunately, if the reason you want to use record syntax is so that you are robust to changes in the number of fields later, this doesn't achieve that. Even worse, in the eyes of the compiler there's no connection between the type a that's in scope and the types of the computation-level variables first and second -- they've been given fresh type variables with no equation relating them to a. So you could only use a as its own thing -- you couldn't use it in a call that needed both first and the type of first as arguments.

If you are actually interfacing with proxy-based functions, then an alternative would be to use the typical proxy-polymorphism together with [] as a light-weight proxy, as in:

f (T {first, second}) = show (typeRep [first]) ++ ": " ++ show (toInteger first, toInteger second)

This only works when you have a thing of the type you care about, and the type you care about has kind *, which definitely aren't always both true.

I like the solution in the comments by 414owen, which is to use a type annotation in the pattern:

f (t {first = first :: a, second}) = ...

This seems the best of the things I've proposed so far, but it does have two unfortunate points; the more important one is that it may require you to repeat non-type-variable bits, e.g. you might have to write first :: Map String (Char, a) or whatever when you really just wanted to name a. That can be mitigated some with partial type signatures, which would let you write _ (_ a), for example, instead of Map String (Char, a). Also the type you want to bind has to be mentioned in some field's type, but that is almost always the case anyway.

英文:

Well, that is unfortunate. Here are some ideas. First, you could match twice.

f x = case x of
    T @a _ _ -> case x of
        T {first, second} -> ...

-- OR, slightly shorter:
f x@(T @a _ _) = case x of
    T {first, second} -> ...

Unfortunately, if the reason you want to use record syntax is so that you are robust to changes in the number of fields later, this doesn't achieve that. Even worse, in the eyes of the compiler there's no connection between the type a that's in scope and the types of the computation-level variables first and second -- they've been given fresh type variables with no equation relating them to a. So you could only use a as its own thing -- you couldn't use it in a call that needed both first and the type of first as arguments.

If you are actually interfacing with proxy-based functions, then an alternative would be to use the typical proxy-polymorphism together with [] as a light-weight proxy, as in:

f (T {first, second}) = show (typeRep [first]) ++ ": " ++ show (toInteger first, toInteger second)

This only works when you have a thing of the type you care about, and the type you care about has kind *, which definitely aren't always both true.

I like the solution in the comments by 414owen, which is to use a type annotation in the pattern:

f (t {first = first :: a, second}) = ...

This seems the best of the things I've proposed so far, but it does have two unfortunate points; the more important one is that it may require you to repeat non-type-variable bits, e.g. you might have to write first :: Map String (Char, a) or whatever when you really just wanted to name a. That can be mitigated some with partial type signatures, which would let you write _ (_ a), for example, instead of Map String (Char, a). Also the type you want to bind has to be mentioned in some field's type, but that is almost always the case anyway.

huangapple
  • 本文由 发表于 2023年5月25日 12:00:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/76328845.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定