Purescript Book - Questions

Hi, almost done with Chapter 4 of the book

Here’s in the repo, will be completing tomorrow last exercises and write some test with runTest /suite.

Some questions…

Difference betrween apply and map

https://spacchetti.github.io/starsuit/Data.Functor.html#v:map

map :: forall a b. (a -> b) -> f a -> f b

map can be used to turn functions a -> b into functions f a -> f b whose argument and return types use the type constructor f

So if I understand correctly, map takes a function (a -> b), a functor f a (a type constructor, like Maybe) and applies the function and returns a new functor f b ?

For example


> map (\n -> n + 3) (Just 5)

(Just 8)

> (\n -> n + 3) <$> Just 5

(Just 8)

https://spacchetti.github.io/starsuit/Control.Apply.html#t:Apply

apply :: forall a b. f (a -> b) -> f a -> f b

Apply can be used to lift functions of two or more arguments to work on values wrapped with the type constructor f.

So here I apply a function wrapped in a context (e.g. Maybe functor/type constructor) to a value or values wrapped with the same type constructor ?

For example


> apply (Just (\n -> n + 3)) (Just 5)

(Just 8)

> Just (\n -> n + 3) <*> Just 5      

(Just 8)

Does this means Apply is an Applicative? Going by the definition of applying a function wrapped in a context to a value wrapped in a context?

How do I write these


> (*) <$> Just 5 <*> Just 3         

(Just 15)

> lift2 (*) (Just 5) (Just 3)       

(Just 15)

with apply?

Can anyone make some example of when this is used in practice?

Applicatives

Does pure support lifting opoperations for any number of function arguments? Or is it other functor(s) part of Applicative typeclass? Any examples?

More generally: for a functor to be part of the Applicative class, it means it needs to have an operation that lift functions of two or more arguments in it (plus the additional laws as per docs)?

Guard

Is Array a of MonadZero typeclass?

I am not clear what happens here in the book:


> import Control.MonadZero

> :type guard

forall m. MonadZero m => Boolean -> m Unit

And then

we can assume that PSCi reported the following type:

Boolean -> Array Unit

Does m stand for monad, so Array is a monad?

Concatenations
Why both these are valid, I thought <> was `String¬ concatenation only

> [1, 2, 3] <> [1]
[1,2,3,1]

> snoc [1, 2, 3] 1
[1,2,3,1]

Examples
Wouldn’t it be great to have additional concrete examples of functions usage in the documentation?

Some have, some don’t – I think it would be great to enrich the docs. For example this is immediate:

cons
cons :: forall a. a -> Array a -> Array a
Attaches an element to the front of an array, creating a new array.

cons 1 [2, 3, 4] = [1, 2, 3, 4]
-- Note, the running time of this function is O(n).

Naming
I find some naming quite obscure fro someone with little knowledge of Lisp / new to the language etc., e.g. unless you know what snoc means a priori, is hard to find a function in Pursuit to simply:

Append an element to the end of an array, creating a new array.

Is there any way this could be improved, perhaps just tags in search?

Thanks for writing-up feedback about your learning experience.

I think you’re getting a bit ahead of the text with these questions about apply and lift2. There’s no mention of either of those in chapter 4. They are explained in detail in chapter 7. We also haven’t even covered type classes yet (chapter 6), which will make it easier to compare Functor, Apply, and Applicative.

My advice for getting through the book as efficiently as possible is to assume everything you need to know is covered in the text (unless explicitly instructed to consult pursuit) and focus on the exercises. Please report if any of the exercises are difficult to complete due to lack of prerequisites, and we’ll address those. Maybe this point should be clarified in the “How to Read this Book” section.

Yes, I agree that every function should have at least one example in the docs. Clojure does a great job of this with easy community-sourced examples. Elm docs also have excellent examples. Feel free to add these examples as you find gaps.

Nope, it works on lots of stuff, and it’s explained in ch6.

You can search for that function in pursuit by its type signature: Array v -> v -> Array v. In this case, the first result is what you want.

2 Likes

To not stifle any curiosity, here are some answers to your other questions that will eventually be covered in the book:

The above summaries of map and apply look good.

Since “The Applicative type class extends the Apply type class”, I’d say that Applicative has all the capabilities of Apply plus more.

I don’t know if this answers your question, but here’s how I’d break-down what’s going on:

lift2 (*) (Just 5) (Just 3)
-- lift2 f a b = f <$> a <*> b
(*) <$> Just 5 <*> Just 3
-- replace operator aliases
(*) `map` Just 5 `apply` Just 3
-- clarifying parenthesis
((*) `map` Just 5) `apply` Just 3
-- reorder map infix
(map (*) (Just 5)) `apply` Just 3
-- result of map
(Just \x -> 5 * x) `apply` Just 3
-- reorder apply infix
apply (Just \x -> 5 * x) (Just 3)

For apply, let’s say you have an optional “action” or function along with an optional value represented by Maybe. If either function or value is Nothing, then it only makes sense for the result to be Nothing too.
It’s similar for lift2 where a Nothing thrown into the mix causes the end result to also be Nothing.
In practice this means you can only get a meaningful output if all your inputs are meaningful too.

Here’s how I’d summarize things:

  1. Start with Functor which gives you map. It allows you to send a wrapped value through a function of one argument. (e.g. send a maybe value through a function that operates on the inner value and get a maybe result).
  2. Now add Apply which gives you apply in addition to map. These can be combined to allow you to send any number of additional arguments to a function. So now you have support for one or more arguments. (e.g. send lots of maybe values through a function and get a maybe result). You can click on the source link for lift2 to see this chaining pattern in their definitions.
  3. Now add Applicative which gives you pure in addition to apply and map. This fills in the remaining missing piece of now supporting functions of zero arguments (aka values). So now you have support for zero or more arguments. An example of this new special case of zero arguments is just starting with a function that is really just a value and converting the value to a maybe result.

Here’s another explanation which might be helpful.

3 Likes

Cons and Snoc: Behind the Names

For context about the cons/snoc naming…

data List a 
  = Nil 
  | Cons a (List a)

If you want to add an element to the front of the list, you would literally Cons the element on:

Cons newElement restOfTheList

cons :: forall a. a -> List a -> List a
cons a list = Cons a list

-- can't recall whether this is infix, infixl, infixr
-- and what precedence...
infix cons as :

1 : 2 : 3 : 4 : Nil 
==
Cons 1 (Cons 2 (Cons 3 (Cons 4 Nil)))

But what happens when you want to add an element to the end of the list? This is similar to consing an element. Since it’s on the end, let’s reverse cons (–>) to get snoc (<–) to show which direction the consing occurs:

1 : 2 : Nil `snoc` 3 == 1 : 2 : 3 : Nil

This naming convention is used for both List and Array in PureScript.

1 Like

@milesfrain thanks for your answers!

Ok search by signature is great, will keep that in mind!

I followed a rabbit hole to understand functors, applicatives and monads, hence then the thinking about ‘what is map’ , ‘what is apply’. I found this post helpful as it introduces wrappers as context using visualisations http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

I just feel getting ahead with that information helps me through the book, but I don’t think is necessary to follow – just how I like to learn gets me to digress a bit :slight_smile:

For <> I found this https://github.com/purescript/documentation/blob/master/guides/Common-Operators.md which only mentions String concatenation, hence my confusion. Now that I checked again it is quite out of date.

For apply yes now I understand – I was missing the Just inside the function!

@milesfrain how to submit examples to add to documentation?

@JordanMartinez yep indeed - had to look it up and it all made sense, thanks for the detailed explanation!

EDIT: I can’t understand what this exercise is asking
(Medium) Characterize those arrays xs for which the function foldl (==) false xs returns true.

At the top of the page it says:

Or, “what is the PureScript equivalent of in JS?”

So it only mentions string concatenation since that’s the only relevant JS operator that is covered by it :wink:

1 Like

It’s asking you to come up with a way of describing those arrays - what do they all have in common which is not true of other arrays? Essentially you’re being asked to express in more natural language what foldl (==) false does.

1 Like

I’ll walk though an example of doing this for lift2.

  1. Click the Source link next to the function on pursuit
  2. Select “Branch: master” from the dropdown in the upper-left corner on the github source page.
  3. Follow these steps to edit the file, and submit a PR.

Here’s my PR to add examples to lift2.

I remember this question being tricky to reason through at first, so one way to get started is to just try a bunch of different input arrays and see if you observe a pattern. Then you can work backwards and think through what foldl is doing with all the parameters.

logShow $ foldl (==) false []
logShow $ foldl (==) false [ true ]
logShow $ foldl (==) false [ false ]
logShow $ foldl (==) false [ true, false ]
logShow $ foldl (==) false [ true, true ]
logShow $ foldl (==) false [ false, false ]

that summary of Functor, Apply, Applicative in terms of argument -arity seems really good, and i can’t recall seeing it before anywhere, wish i’d had that when i was learning it.

1 Like

That guy articles are pretty good. When I started learning some CS I used his algorithm book and was a very gentle introduction @afc

What is meant by -arity?

arity is the number of parameters to a function

1 Like

@milesfrain @JordanMartinez done added ch4 tests if you want to check https://github.com/p2327/purescriptbook/blob/master/ch4/test/Main.purs

If you’d like to have the tests you write incorporated into the book, feel free to sign-up for a chapter here. Looks Scott just tackled Ch4.

1 Like

Moving on the book to chapter 6 I am not sure I understand what is asked in the exercise:

(Easy) The following declaration defines a type of non-empty arrays of elements of type a :

data NonEmpty a = NonEmpty a (Array a)
  • Write an Eq instance for the type NonEmpty a which reuses the instances for Eq a and Eq (Array a) .

I checked the source code and the instance for Eq for Array sends me to a JS implementation:

-- from PS source 
instance eqArray :: Eq a => Eq (Array a) where
  eq = eqArrayImpl eq
-- Eq.js
exports.eqArrayImpl = function (f) {
  return function (xs) {
    return function (ys) {
      if (xs === ys) return true;
      if (xs.length !== ys.length) return false;
      for (var i = 0; i < xs.length; i++) {
        if (!f(xs[i])(ys[i])) return false;
      }
      return true;
    };
  };
};

I wrote my own implementations of Eq and show for the NonEmpty type like the below, but I am not sure I am doing what the exercise is asking?

-- Ex 1
instance eqNonEmpty :: Eq a => Eq (NonEmpty a) where
    eq (NonEmpty x xs) (NonEmpty y ys) = (eq x y) && (eq xs ys)

instance showNonEmpty :: Show a => Show (NonEmpty a) where
    show (NonEmpty x xs) = show x <> show xs

I’m pretty sure that’s exactly what the exercise expects you to do for the Eq instance. The Show instance probably doesn’t quite do what you’d want it to, you could give it a try in the repl to see.

Thanks @hdgarrood cool! So is simply asking use eq for both combined…

Yes I don’t think show is correct…

Maybe

-- snip
show (append [x] xs)

Not sure I’m reading too much into this but what’s the use of such data type, for example with the above implementation I get:

-- omit required imports 
> x = NonEmpty 1 [2, 3]
> show x
”[1, 2, 3]"

I wonder if there might be a somewhat more practical / real-world way to have an exercise on type constraints than the NonEmpty example, but I haven’t come up with something concrete :frowning:

The goal is for show x to print the PureScript code required to construct x, but your implementation above will construct an Array instead of a NonEmpty.

1 Like

I would argue that being able to distinguish empty versus non empty arrays in a way that the compiler can check for you (which is what NonEmpty does) is very practical - it allows you to rule out bugs. For instance, you might want to have a parser produce an array of error messages if it fails, but you want to guarantee that if it does fail, the array of failures is nonempty. The “foreign” library does exactly this.

1 Like

@paulyoung The goal is for show x to print the PureScript code required to construct x , but your implementation above will construct an Array instead of a NonEmpty .

Ok but then I am not sure I understand - the definition of nonempty given is of an element of type a with an array of the same type, e.g. NonEmpty 0 [4, 5]

How would I represent this with show?

I checked Data.NonEmpty in the source and this is the Show instance

instance showNonEmpty :: (Show a, Show (f a)) => Show (NonEmpty f a) where
  show (a :| fa) = "(NonEmpty " <> show a <> " " <> show fa <> ")"

So I tried this in the repl

> import Data.Show
> import Control.Plus        
> import Data.NonEmpty   
> nonEmptyX = NonEmpty 1 [1, 3, 4]  
> show nonEmptyX
"(NonEmpty 1 [1,3,4])"

But my initial implementation

instance showNonEmpty :: Show a => Show (NonEmpty a) where
    show (NonEmpty x xs) = show x <> show xs

Looks similar to the one in the source? So I just need to do:

instance showNonEmpty :: Show a => Show (NonEmpty a) where
    show (NonEmpty x xs) = "(NonEmpty " <> show x <> " " <> show xs <> ")"

@hdgarrood gotcha - I have a question tho

I can construct a NonEmpty by doing

a = NonEmpty [1, 2]

So the compiler knows a is NonEmpty.

But if I try show a, I get an error, is this intended?

> a = NonEmpty [1, 2]
> show a
Error found:
in module $PSCI
at :1:1 - 1:7 (line 1, column 1 - line 1, column 7)

  No type class instance was found for
  
    Data.Show.Show (t1 (Array Int) -> NonEmpty t1 (Array Int))
  
  The instance head contains unknown type variables. Consider adding a type annotation.

Yes, that error is intended. If the error message were a bit better it might say “maybe you haven’t applied a function to enough arguments?” The reason is that you haven’t fully constructed a NonEmpty: the NonEmpty constructor takes two arguments. A NonEmpty Int consisting of the elements 1, 2 would be constructed with NonEmpty 1 [2].

1 Like