DEV Community

loading...

Haskell quick start

dannypsnl profile image 林子篆 Originally published at dannypsnl.github.io on ・5 min read

Haskell resource is a little bit outdated and if you take a look at https://www.haskell.org/documentation/ you would found there are several tutorials, make choose one be a hard thing. So I decide to make a simple quick start to record how I start with Haskell.

Before we typing any code, we have to install Haskell, please take a look at https://www.haskell.org/downloads/.

Binding

Let's start from a hello world example:

main :: IO ()
main = do
  putStrLn "Hello, World!"

We call it helloworld.hs, now run runhaskell helloworld.hs, should get: Hello, World! as an output on the screen.

We can find main appears twice. We call the first one as type binding, the second one as binding. Type binding shows the type of main, binding shows what would main do. At here IO is a kind of special type, its called Monad, and we are not going to explain it but can see that the body of main has a notation do, under do notation would be a special syntax that helps us create a DSL for certain domain, here is IO.

Keep going we add read into our program:

import           System.IO

main :: IO ()
main = do
  who <- getLine
  putStrLn ("Hello, " ++ who)

getLine would read the text you typed until a newline in as a string. Here we see a new symbol <-, it might weird at the beginning that it means = in do notation(we actually can use let who = replace <-). What <- do is make a Monad a be a. Then we can use it at the next function. ("Hello, " ++ who) is required, since Haskell doesn't use function_name(parameter*) this form, distinguish the argument belongs to which function call can be quite complex even undecidable and to make our brain work easier better add () for all function call, and tend to use temporary variables for the argument to explain code better.

Now, you probably already found Haskell does not use {} for block body. In Haskell, indent has meaning. For example:

import           System.Environment
import           System.IO

main :: IO ()
main = do
  args <- getArgs
  case args of
    [] -> putStrLn "no arg"
    [one] -> putStrLn "one arg"
    rest ->
      case rest of
        [one, two] -> putStrLn "two args"
        _          -> putStrLn "many args"

We use getArgs from System.Environment and case of syntax. Can see how indent makes the case syntax became different blocks. Of course, I would say better do not have too many indents, its quite easy to confuse yourself.

p.s. I didn't explain list patterns in matching, I hope the output is clean enough to understand those list patterns.

Now let's back to type binding and binding, so why we have to write them twice? In fact, we can ignore type binding:

main = do
  putStrLn "Hello, World!"

I really don't like this idea, at the beginning it sounds like a good idea: Let compiler inferences and tells us what is the type of binding. This is a trap when the compiler does inference, it has to rely on the using case to guess the type of binding. We have a trivial example:

id x = x

When we do function call by id 1, x might be Int, when we call it by id "wow", x might be String. Now imagine a bigger function and the type of it was always changing since we didn't give it a type, can anyone work with it quite happy? I don't think so. So make sure you give it a good type. In fact, the semantic checker would warn those global level binding which didn't with type binding. Well, we can't ignore any type of bindings? I think that's ok in where clause. In Haskell where clause can put some helpers which we don't want other functions to use it.

foo = bar
  where
    bar = 1

In where we can put many private bindings, and since they are usually short and easy to understand, without type binding might be ok, but if you found without type binding they would be hard to understand, then please still give it a type.

Type

Now, we need to define some new types. In Haskell, we usually use data keyword to create a new type. data syntax is quite complex, we can take a look at few examples to understand it.

The first example creates a Bool

data Bool = True | False

To use it: False, True. We call True and False as constructors of Bool. You can see that | separate different constructors. This is called algebra data type. In fact, constructors can with arguments.

data Position = MakePosition Double Double

We can use like: MakePosition 1.0 0.0

When we do pattern matching on the type made by data, we can use their constructors:

case bool of
  True -> -- ...
  False -> -- ...
-- or
case pos of
  MakePosition x y -> -- ...

p.s. -- is the comment in Haskell

Well, MakePosition makes a new question: Can we use pos in the branch of MakePosition? Yes, by @ pattern, we can get the constructors:

case pos of
  p1@MakePosition x y -> -- ...

It seems useless because pos is p1 in case. But it's very helpful when Position has several constructors. A nice thing is Haskell would do exhaustiveness checking to make sure all variants of constructors were covered by your pattern matching.

Keep move forward, sometimes we would like to create a type and some operations that can be reused by different type-arguments. It called type parameters. You probably already found that in Haskell all of the types were started with upper case. That's because the start of the lower case was reserved for the type parameter.

The first example is Maybe:

data Maybe a =
  Just a
  | Nothing

What is Maybe for? In Haskell, we do not have the bottom type. What's the bottom type? In Java, every object can be null, null can be seen as a value of bottom type. The bottom type represents a subtype of any type, so its value can be the value of any type. Of course, null in Java is not so correctly means a bottom type since the primitive type work under another system, but it's good enough to make we keep going.

So we actually need a type to represent nothing, that is Maybe, Maybe has two constructors, for has value: Just a and no value: Nothing. But doing operations on Maybe is annoying, if without Control.Applicative. We can see how it work with Applicative:

import Control.Applicative

(+) <$> Just 2 <*> Just 3
-- Just 5
(*) <$> Just 2 <*> Just 3
-- Just 6
(+) <$> Just 2 <*> Nothing
-- Nothing

You can understand Applicative like this: + takes two parameters, <$> means we start to give it arguments, use <*> join more arguments if there is more than one argument.

There has a structure-like syntax for constructors:

data Position = MakePosition {
  getX :: Double
  , getY :: Double
  }

An unfortunate problem is getX and getY is not in the global environment, so if now we add:

data Wow = MakeWow {
  getX :: Double
  , getY :: Double
  }

The compiler would complain about there are redefined symbols.

Conclusion

I hope you already learn a little thing that enough to start programming with Haskell. There still have many features were not mentioned in the article. Originally as https://dannypsnl.github.io/docs/golang-quick-start/, I want to show all major features in Haskell, but it probably would be really hard. Explain Monad might need an alone article. And high-order function and Kind also quite not suitable in an article prepared for beginner. Understand fold, map and recursive also be out of the topic of the article, so I would write the others for them. Thanks for the read.

Discussion (0)

pic
Editor guide