1

I'm trying to convert some simple C# code to Haskell. So say I've got a simple immutable "database" type that is just a record with various list fields. So, say

data Person = Person { }
data Book = Book { }
data Database = Database { employees :: [Person], books :: [Book], customers :: [Person] }

Now I want to create a typeclass that represents a "view", or essentially a "table" of that DB.

class Table r t where -- r is the record type (e.g. Person or Book)
  getRecords :: t -> Database -> [r]
  setRecords :: t -> [r] -> Database -> Database

Then I can create instances that represent each of those tables:

data ET = EmployeeTable
instance (Table Person) ET where
  getRecords t db = employees db
  setRecords t records db = Database records (books db) (customers db)

This is what I have, and it works, but only if {-# LANGUAGE MultiParamTypeClasses #-} is included. Otherwise the definition of the Table typeclass fails.

Not a big deal in itself: it compiles and works, but a quick read on MultiParamTypeClasses alludes to potential complications down the line (I haven't taken the time to fully grok them yet).

The weird thing for me though is that this is very straightforward in C#. Assuming simple immutable definitions of the record/DB class, it's simple to define the interface, and then the implementations follow without issue.

interface ITable<TRecord> {
    TRecord[] GetRecords(Database db);
    Database SetRecords(TRecord[] records, Database db);
}

So really that's the essence of this question. Is there a more idiomatic way to translate the functionality accorded by the above ITable<TRecord> interface from C# to Haskell? My understanding is that C# interfaces are closest to Haskell typeclasses, so that's what I'm trying to do. But I find it suprising that something as simple as a generic interface requires a language extension in the highly-touted type system in Haskell.

(N.B. why do I want to do this? The above is a bit simplified for brevity's sake, but in general, if I make the fleshed-out Person and Book instances of a Record typeclass that just has getId, then I can support CRUD functions for the whole database very generically: I only have to define these functions once for the Table typeclass, and they'll apply to all tables in the DB automatically. Here's the full code a sample usage of it at the bottom, and its C# equivalent. https://gist.github.com/daxfohl/a785d1ff72b921d7e90b70f625191a1c. Note in Haskell deleteRecord doesn't compile either since the type of r cannot be deduced, whereas in C# it compiles fine. This adds to my thought that maybe MultiParamTypeClasses is not the right approach. But if not, then what is?)

Update

Okay it sounds from the comments like MultiParamTypeClasses is fine. So now my remaining question is how to fix the linked gist such that deleteRecord will compile?

8
  • 4
    If your issue is with MultiParamTypeClasses, then clear that out of your head. This is a pretty standard extension to enable. The reason why these extensions exist is to allow different Haskell compilers to implement new behavior beyond the Haskell Standard, which can eventually become standard. I'll note that the "dilemmas" linked to by prime.haskell.org/wiki/MultiParamTypeClasses is an article last modified 11 years ago. Since then it's become easier to work with. Commented Feb 27, 2017 at 16:27
  • 1
    Another one of your complaints is that you seemingly have to reimplement the getId part multiple Times. This is somewhat true without further techniques, and it comes down to how Haskell does name resolution. You could look at the lens library, which solves a lot of these problems at the cost of several compiler extensions. Commented Feb 27, 2017 at 16:27
  • 1
    Modern Haskell code often uses a lot of language extensions. Don't be afraid to turn them on as needed. There are a few that should be avoided (like overlapping/incoherent instances, or the currently broken impredicative types), but many extensions are harmless. Commented Feb 27, 2017 at 16:31
  • About "a generic interface requires additional language support in the highly-touted type system in Haskell" -- I can't follow this. Are you referring as "additional language support" the extension you had to turn on? It's one of the most well-known ones... we do not program in Haskell'98 anymore. Commented Feb 27, 2017 at 16:35
  • @chi Yes, I'm referring to the extension. Will update the question to be more explicit. Commented Feb 27, 2017 at 16:36

1 Answer 1

3
deleteRecord :: (Table r t) => t -> Int -> Database -> Database

This is a problem. There's an r before the => but no r after the =>. Given a function call like deleteRecord bookTable 1 db, Haskell has no idea which r you are talking about. Though r should be completely determined by t, Haskell has no way of knowing that. Indeed, no one disallows these instance definitions:

instance Table Foo Bar
instance Table Foo Baz
instance Table Qux Bar
instance Table Qux Baz

There is nothing in the "table" types that could prevent this. They are just empty tags without any real data.

The fact that there is only one instance of interest in your module is irrelevant, Haskell cannot guarantee anything about other modules.

So what are your options here?

  1. Get rid of table types altogether. Record types could be enough.
  2. Use another Haskell extension, FunctionalDependencies.
  3. Use yet another Haskell extension, TypeFamilies.

The last two extensions allow for creation of generic containers among other things, which is what your database tables essentially are.

Here's how Table type class would look with the first extension:

class Record r => Table r t | t -> r where ...

The notation t -> r means that r is completely determined by t (IOW r functionally depends on t). Once Haskell sees an instance Table Foo Bar, it knows there can be no instance Table Qux Bar (the compiler will signal an error should it see a conflicting definition). This way deleteRecord is well-formed. r is not in the signature but it's OK: t is known and r is a function of t.

I let you figure out TypeFamilies by yourself. It's a more popular solution these days. FunctionalDependencies is an older extension, it is simple to understand but can lead to complications in some corner cases. Don't worry about them, you will be a master of Haskell before you see any.

Sign up to request clarification or add additional context in comments.

2 Comments

Going with (1), I changed Table from a class to a record with getRecords and setRecords members: data Table db a = Table { getRecords :: db -> a, setRecords :: a -> db -> db }. Then the CRUD ops just needed types changed to e.g. deleteRecord :: Record r => Table Database [r] -> Int -> Database -> Database. Finally the objects could be defined using lambdas: bookTable = Table { getRecords = books, setRecords = \records db -> Database (employees db) records (customers db) } and all compiled. Is this more idiomatic in Haskell?
Option 4. Allow ambiguous types (and use type applications). This would be probably the worst option for this case, since there's a clear fundep here.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.