Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ADTs vs. Typeclasses -- Canonical use

Tags:

haskell

I'm torn between two implementations of a certain data structure, and input from the Haskell community as to what is right/standard would be appreciated.

Data Types

Take, for example, a ADT "Server" which defines several servers as nullary data constructors.

data Server = Server1
            | Server2
            | Server3

Now, for each of these servers I want to have (among other things) the ability to get an IP address. Assuming I can code these statically, I can have some function "getURL" and pattern match.

getUrl :: Server -> String
getUrl Server1 = "192.168.1.1"

and etc. Now any function which uses servers can put Server in the type and call getURL.

serverStuff :: Server -> IO ()

This method seems to have the benefit of simple, non-polymorphic functions at the expense of having lots of pattern matching in getURL. Additionally, if the programmer adds a Server but forgets to add the pattern to getURL, they will get a runtime error without warning unless they compile with -Wall.

Typeclasses

Attacking the same problem with typeclasses, I can break out my multi-constructor ADT into a set of ADTs specific to the server and create a type class for URL.

data Server1 = Server1
data Server2 = Server2
data Server3 = Server3

class Server a where
    getUrl :: a -> String

instance Server Server1 where
    getUrl Server1 = "192.168.1.1"

and etc. Now instead of the simple non-polymorphic function I used before, I have to create something like

serverStuff :: Server a => a -> IO ()

and deal with the implications of ad-hoc polymorphism (function specialization and the like).

On the bright side, the typeclass method easy to expand, breaks up the pattern matching into smaller chunks, allows for greater abstraction e.g. grouped servers (data ServerCenter1 = Server1 | Server2 | Server3), and, while you can still get runtime errors (without compiler warning) if you don't declare getUrl, you're at least forced to make that decision when you create the instance.

So, I'm torn but leaning toward instances as a better way of doing things. Is there a standard way to handle this issue, or is it a "whatever seems clean" type of thing?

like image 306
Elliot Robinson Avatar asked Nov 28 '12 16:11

Elliot Robinson


People also ask

What are the different types of ADTs?

There are two basic categories of ADTs: A product type is essentially a way of sticking multiple values inside of one - a Tuple, or something that’s very similar to one. Case classes are the prototypical product type:

What is an abstract data type (ADT)?

Abstract Data Types (ADTs) ●Given a problem, you are required to filter out irrelevant details. ●The result is an abstract data type (ADT) , whose interface consists of a list of (unimplemented) operations. 2 pe : (DS) S s tÓ ! ! T Data Structure terface add() () () t lt es¥ay )¥st ●Supplier’s Obligations:

What is the difference between ADT and classes?

Difference between ADT and Classes? ADT is the set of operations. ADT's are mathematical abstractions. Implementation independent data description that specifies the contents, structure and legal operations on the data. Does this mean that ADT are same as classes or am i confusing both together ? You can almost think of an ADT like a plan.

What are algebraic data types (ADTs)?

Algebraic Data Types (ADTs for short) are a way of structuring data. They’re widely used in Scala due, mostly, to how well they work with pattern matching and how easy it is to use them to make illegal states impossible to represent. There are two basic categories of ADTs:


1 Answers

If you are positive that the only information you need your server type to contain, I would just implement them as a newtype around a string:

newtype Server = Server { getURL :: String }

Making it a full record (as in hammar's comment) would allow you to add information while changing only the constructors, at the expense of GeneralizedNewtypeDeriving.

In general, I would use types to represent classes of things and variables to represent particulars, so that nullary constructors are used only to represent abstracts, e.g. data Status = Published | Draft (or the builtin Bool). Hardcoding data (such as ip addresses) into the type system or functions should be avoided, unless there is a specific reason.


If you want server specific behaviors, it is easy to add fields to the record:

data Server = Server {
              getURL      :: String
            , doSomething :: a -> IO () --Or any other functional signature
            }

However, I would advise against doing this, because it makes other code obscure:

runSomething :: Server -> a -> IO ()
runSomething server arg = (doSomething server) arg

can do absolutely anything, and you need to locate the last update of that field to determine what (since functions have no Show instance). If the dependency depends on some property of the server, I would favor encoding that property and then dispatching on that, e.g.

data ServerType = Production
                | Development

data Server = Server {
              getURL     :: String
            , serverType :: ServerType
            }

runSomething :: Server -> a -> IO ()
runSomething server arg = case serverType server of
                              Production  -> foo arg
                              Development -> bar arg

I consider this approach superior to hardcoding server names into the function, because it explains why a given server has a given behavior (and makes changing the behavior of a particular server more local), and to having functions in the record fields, as it makes it easier to tell what a given invocation of runSomething will do (as one can inspect and log the ServerType).

like image 181
isturdy Avatar answered Jan 21 '23 13:01

isturdy