Reputation: 10781
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?)
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?
Upvotes: 1
Views: 220
Reputation: 119847
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?
FunctionalDependencies
. 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.
Upvotes: 3