I think and correctly use singleton types in Haskell?

I want to create several incompatible but otherwise equal data types. That is, I would like to have a parameterized type Foo a , and functions such as

 bar :: (Foo a) -> (Foo a) -> (Foo a) 

not caring about what a . To clarify further, I would like the type system to stop me from doing

 x :: Foo Int y :: Foo Char bar xy 

while I at the same time do not care about Int and Char (I do not care that they do not match).

In my actual code, I have a type for polynomials over a given ring. I don’t care what is indefinite, until the type system prevents me from adding a polynomial in t with polynomial in s. So far I have solved this by creating typeclass Indeterminate and parameterizing my polynomial type as

 data (Ring a, Indeterminate b) => Polynomial ab 

This approach seems completely natural for the Ring part, because I care about which particular ring is given for the polynomial. It feels very far-fetched for the Indeterminate part, as described below.

This approach works great, but feels far-fetched. Moreover, this part:

 class Indeterminate a where indeterminate :: a data T = T instance Indeterminate T where indeterminate = T data S = S instance Indeterminate S where indeterminate = S 

(and so on, perhaps a few more uncertainties). This is strange and wrong. Essentially, I'm trying to require Indeterminate instances to be single (for that matter). A sense of strangeness is one of the indicators that I may be wrong in this. Another is the fact that I end up having to comment on a lot of my Polynomial ab , since the actual type b often cannot be inferred (which is not strange, but annoying anyway).

Any suggestions? Should I continue to do it this way, or am I missing something?

PS: Do not be offended if I do not immediately respond or accept the answers. I will not be able to return for a few days.

+6
singleton haskell
source share
1 answer

First of all, I'm not sure about this:

 data (Ring a, Indeterminate b) => Polynomial ab 

... does what you expect. The contexts on data definitions are not very useful - see the discussion here for some reasons, so most of them force you to add additional annotations without actually providing many additional type guarantees.

Secondly, do you really care about the "indefinite" parameter, in addition to making sure that the types are stored explicitly? A fairly standard way of doing such things is what is called phantom types β€” optionally, parameters in the type constructor that are not used in the data constructor. You will never use or need a value like phantom, so functions can be as polymorphic as you want, for example:

 data Foo ab = Foo b foo :: Foo ab -> Foo ab foo (Foo x) = Foo x bar :: Foo ac -> Foo bc bar (Foo x) = Foo x baz :: Foo Int Int -> Foo Char Int -> Foo () Int baz (Foo x) (Foo y) = Foo $ x + y 

Obviously, this requires annotations, but only in those places where you intentionally add restrictions. Otherwise, the output will work fine for the phantom parameter.

It seems to me that the above approach should be sufficient for what you are doing here - a business with singleton types is mainly related to bridging the gap between more complex level types and regular value level calculations, creating a proxy type for values. This can be useful, for example, for designating vectors with types indicating their basis, or for marking numerical values ​​with physical units - in both cases, when the annotation makes more sense than just "indefinite, called X."

+7
source share

All Articles