Data declaration vs type classes

Dear haskellers, What is the difference between writing data Task = GroceryTask String | LaundryTask Int doTask :: Task -> IO () doTask (GroceryTask s) = print "Going to " ++ s doTask (LaundryTask n) = print (show n ++ " pieces washed" and class Task a where work :: a -> IO () data GroceryTask = GroceryTask String data LaundryTask = LaundryTask Int instance Task GroceryTask where .. instance Task LaundryTask where .. doTask :: Task a => a -> IO () doTask = work They seem to be similar functionality wise, except that one is on the data level and another is on the class level. How should one go about deciding to use data or class? Is there a semantic difference? Which is more appropriate here? Happy new year, Hon

First one is closed: there is a very clear list of all possibilities, kept in one place. Even if it's exported, it's impossible to add anything to the list of tasks without modifying that module.
Second is open; if it's exported, users of your module can add their own tasks.
On the other hand, adding new function that works on all tasks is, in the first case, simple: you can just write it in the same way as your `doTask`. Users can do that without modifying the module. In the second case you have to change your `Task` class if you want to add a function.
08.01.2016, 12:56, "Lian Hung Hon"
Dear haskellers,
What is the difference between writing
data Task = GroceryTask String | LaundryTask Int
doTask :: Task -> IO () doTask (GroceryTask s) = print "Going to " ++ s doTask (LaundryTask n) = print (show n ++ " pieces washed"
and
class Task a where work :: a -> IO ()
data GroceryTask = GroceryTask String data LaundryTask = LaundryTask Int
instance Task GroceryTask where ..
instance Task LaundryTask where ..
doTask :: Task a => a -> IO () doTask = work
They seem to be similar functionality wise, except that one is on the data level and another is on the class level. How should one go about deciding to use data or class? Is there a semantic difference? Which is more appropriate here?
Happy new year, Hon ,
_______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe

How should one go about deciding to use data or class?
class: class lets specify more than one method. when you define instance yet do not implement all methods, compiler warns. if you try to call class method without an instance for that type, compiler warns. pattern matching: compiler does not warn if methods do not match every constructor of the data type. one way to decide if not sure, is to pick one way which seems easier to refactor. when more code is written, it usually becomes obvious if this approach does not fit. then refactor.
Which is more appropriate here? depends on the rest of the code. if this is it, then there is no real difference.

On 01/08/2016 11:38 AM, Imants Cekusins wrote:
How should one go about deciding to use data or class?
class: class lets specify more than one method. when you define instance yet do not implement all methods, compiler warns. if you try to call class method without an instance for that type, compiler warns.
pattern matching: compiler does not warn if methods do not match every constructor of the data type.
Well, there is -fwarn-incomplete-patterns, which should be included in -Wall, which does exactly this.
one way to decide if not sure, is to pick one way which seems easier to refactor. when more code is written, it usually becomes obvious if this approach does not fit. then refactor.
A few things to keep in mind: If you define a type class and instances for Int and String, and later want to add another case for String, you have to add a newtype, otherwise the compiler can not differentiate. Additionally, adding a constructor with multiple fields gets complicated if you choose the type class solution, here you have to add an instance for a tuple. Also you need FlexibleInstances as soon as you want an instance for String or a Tuple more specific than (a, b) (or introduce newtypes). You can also not process a Task if it is hidden in a class. For example, how do you implement doOnlyShoppingTask? In the end you restrict yourself with a type class about the things you can do with the data. So when is this useful? I would argue, if you are writing a library and want your users to be able to define their own tasks. Otherwise I think abstracting a data type with a type class is not worth the hassle.
Which is more appropriate here? depends on the rest of the code. if this is it, then there is no real difference.
Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe

Well, there is -fwarn-incomplete-patterns, which should be included in -Wall, which does exactly this.
cheers Jonas. will try this. another thing: class lets reuse the same method name for several types. with pattern matching, different types require different function names. it is possible to place methods in different modules and call them qualified, but class solution seems cleaner. basically, classes are very convenient for standardization and extending code. a bit like Java interfaces :-P

On Fri, Jan 08, 2016 at 05:55:45PM +0800, Lian Hung Hon wrote:
How should one go about deciding to use data or class? Is there a semantic difference?
Classes are not first class citizens in Haskell, and it's very hard to pass them around, manipulate them and compute with them without using non-standard and awkward techniques.
Which is more appropriate here?
Almost certainly data. My rule of thumb is to only introduce a typeclass once it becomes incredibly repetitive passing around the data explicitly. Tom

it's very hard to pass them around, manipulate them and compute with them without using non-standard and awkward techniques.
well here is one simple use case when class is very convenient: class ConvertByteString a where toByteString::a -> ByteString fromByteString::ByteString -> a no problems defining instances of this class, passing and calling them whatsoever.

On 8 January 2016 at 12:26, Imants Cekusins
it's very hard to pass them around, manipulate them and compute with them without using non-standard and awkward techniques.
well here is one simple use case when class is very convenient:
class ConvertByteString a where toByteString::a -> ByteString fromByteString::ByteString -> a
no problems defining instances of this class, passing and calling them whatsoever.
One problem with this class would be if you convert String or Text: what encoding would you use? Probably UTF8, but there are others, and if you need those you need a newtype at least. Erik

you convert String or Text: what encoding would you use?
let's say, this is very specific conversion where newtypes are used a lot. There are many different formats for Int (even the same type of int), String may be ascii, UTF8, ISO-..., you name it. using class does not make a difference re: type definition in this case.

Dear all,
Thanks for the opinions. I'll go with type classes for now, because as
Miguel said, I want it to be open :)
Regards,
Hon
On 8 January 2016 at 19:46, Imants Cekusins
you convert String or Text: what encoding would you use?
let's say, this is very specific conversion where newtypes are used a lot. There are many different formats for Int (even the same type of int), String may be ascii, UTF8, ISO-..., you name it.
using class does not make a difference re: type definition in this case. _______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe

In case of the data approach, `GroceryTask` and `LaundryTask` are the
same type: `Task`. Hence you can have some kind of "dynamic"
polymorphism (or "dynamic" dispatch) by storing a list of homogeneous
types (`Task`) with heterogeneous behaviors.
For example, imagine you want to store a todo list and do all task of
the todo list.
data Task = GroceryTask | LaundryTask
doTask GroceryTask = putStrLn "grocery"
doTask LaundryTask = putStrLn "laundry"
todoList :: [Task]
todoList = [GroceryTask, LaundryTask, GroceryTask]
doAllTasks :: [Task] -> IO ()
doAllTasks tasks = mapM_ doTask tasks
However, In the case of the class approach
data GroceryTask
data LaundryTask
class Task t where
doTask :: t -> IO ()
instance Task GroceryTask where
doTask t = putStrLn "grocery"
instance Task LaundryTask where
doTask t = putSTrLn "laundry"
doAllTask :: [?????] -> IO ()
In this case, GroceryTask and LaundryTask are NOT the same type, hence
the "????", you cannot create a list which stores different Tasks and
returns apply
However you can still wrap them inside a sum type :
data DoableTask = DoableGrocery GroceryTask | DoableLaundry LaundryTask
instance Task DoableTask where
doTask (DoableGrocery t) = doTask t
doTask (DoableLaundry t) = doTask t
(Open question: is there a hack / tool / library / Template Haskell
solution to generate this kind of stuff ?)
There is other solutions, you can partially apply the doTask function,
for examples:
todoList :: [IO ()]
todoList = [doTask GroceryTask, doTask LaundryTask, doTask GroceryTask]
(Another open question, is there a simple solution to do a map over an
literal heterogeneous list to get an homogeneous one?)
Thank to laziness, this works, but can be really boring to implement.
There is other solution using existential types or heterogeneous
lists. I'm still looking for a good discussion about which one to use
when we focus on performance.
So, finally, there is no simple solution. If your type is close and
really represents a choice between a set of possibilities and that you
know you want a kind of dynamic dispatch, definitely go for the data
approach. Else, the class approach is easier to extend at the cost of
a lot of boilerplate when you want dynamic dispatch...
On Mon, Jan 11, 2016 at 12:44 PM, Lian Hung Hon
Dear all,
Thanks for the opinions. I'll go with type classes for now, because as Miguel said, I want it to be open :)
Regards, Hon
On 8 January 2016 at 19:46, Imants Cekusins
wrote: you convert String or Text: what encoding would you use?
let's say, this is very specific conversion where newtypes are used a lot. There are many different formats for Int (even the same type of int), String may be ascii, UTF8, ISO-..., you name it.
using class does not make a difference re: type definition in this case. _______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
_______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe

You can do this using ExistentialQuantification.
On Jan 11, 2016, at 10:15, Guillaume Bouchard
wrote: doAllTask :: [?????] -> IO ()
In this case, GroceryTask and LaundryTask are NOT the same type, hence the "????", you cannot create a list which stores different Tasks and returns apply
participants (8)
-
Erik Hesselink
-
Guillaume Bouchard
-
Imants Cekusins
-
Jonas Scholl
-
Lian Hung Hon
-
Miguel Mitrofanov
-
Tom Ellis
-
Will Yager