Data declaration vs type classes

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
12 messages Options
Reply | Threaded
Open this post in threaded view
|

Data declaration vs type classes

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
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: Data declaration vs type classes

MigMit
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" <[hidden email]>:

> 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
> [hidden email]
> http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: Data declaration vs type classes

Imants Cekusins
> 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.
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: Data declaration vs type classes

Jonas Scholl
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
> [hidden email]
> http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
>


_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe

signature.asc (484 bytes) Download Attachment
Reply | Threaded
Open this post in threaded view
|

Re: Data declaration vs type classes

Imants Cekusins
> 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
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: Data declaration vs type classes

Tom Ellis
In reply to this post by Lian Hung Hon
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

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: Data declaration vs type classes

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.
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: Data declaration vs type classes

Erik Hesselink
On 8 January 2016 at 12:26, Imants Cekusins <[hidden email]> wrote:

>> 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
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: Data declaration vs type classes

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
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: Data declaration vs type classes

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 <[hidden email]> 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
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe


_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: Data declaration vs type classes

Guillaume Bouchard
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 <[hidden email]> wrote:

> 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 <[hidden email]> 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
>> [hidden email]
>> http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
>
>
>
> _______________________________________________
> Haskell-Cafe mailing list
> [hidden email]
> http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
>
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: Data declaration vs type classes

Will Yager
You can do this using ExistentialQuantification.

> On Jan 11, 2016, at 10:15, Guillaume Bouchard <[hidden email]> 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
>
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe