Trying to modify a dice demo... then got stuck

I found this dice demo in purescript-cookbook, I want to make a little difference and see what would happen

module Main where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import Effect.Random (randomInt)

main :: Effect Unit
main = do
  log result
  where
  result = (show (randomInt 1 6))

Then error found:

Compiling Main
Error found:
in module Main
at src/Main.purs:12:13 - 12:33 (line 12, column 13 - line 12, column 33)

  No type class instance was found for

    Data.Show.Show (Effect Int)


while applying a function show
  of type Show t0 => t0 -> String
  to argument (randomInt 1) 6
while inferring the type of show ((randomInt 1) 6)
in value declaration main

where t0 is an unknown type

I want to

  • exeute randomInt 1 6 then
  • put the generated integer in show then
  • put the generated string in result

Question 1: Why doesn’t it work as I thought?

I try to figure out what does <- mean in the original example

No results on pursuit

I tried in https://github.com/purescript/documentation/blob/master/language/Syntax.md

I tried to figure out what is bind , found this
https://pursuit.purescript.org/packages/purescript-prelude/4.1.1/docs/Control.Bind#v:bind

Still don’t know what’s the usage of <- , and what does it mean :persevere:

Question 2: How to quickly figure out the usage of a symbol/keyword?

1 Like

If you’re dealing with a regular function, Pursuit is the right place to look. In this case <- is not a regular function, it’s part of the language syntax. The language reference in the purescript/documentation repo is not a beginner resource; if this is your first time coming across <- I’d recommend the chapter of the PureScript book which covers the Effect monad: https://book.purescript.org/chapter8.html

Thanks again for sharing the steps you took to figure this out on your own. This is valuable feedback on how folks can get stuck, despite doing a reasonable job consulting the available reference material (e.g. Pursuit and Docs repo).

Unfortunately, it seems like most of the resources expect you to read the book first, which can be a pretty significant initial time investment. We also don’t clearly state this as a prerequisite.

I think we should also enable the learn-from-examples approach by providing a better syntax reference. Perhaps we list symbols and keywords that are built into the language, along with links to where the reader can learn more.

For example:

Symbol or Keyword Used For
do “do notation”(link)
<- “do notation”(link), “pattern guards”(link)
-> “basic functions”(link), “lambda functions”(link), “case expressions”(link)
. “record access”(link), “generic functions / forall”(link)
:: “type signatures”(link)
\ “lambda functions”(link)
_ “ignored type”(link), “anonymous argument”(link)

I just pointed to the not-so-beginner-friendly language reference for a few of those links above. Ideally, we’d make this content accessible to beginners too by either:

  • Add a link to a beginner’s guide in each section of the reference.
  • Link to a beginner-friendly guide first, that then points to the reference.
  • Keep both levels of detail in the reference, but start with a terse summary, and then follow-up with more descriptive examples further down in the page.

Maybe this symbol and keyword lookup can be built into Pursuit.

This video delivers a really excellent overview of important language concepts in a beginner-friendly way (including “do notation”). Note that we use pure instead of return.


I think the term pure is another source of confusion for beginners. When I first saw that keyword, I assumed it was making the returned value “more pure”, when in-fact the value returned is “less pure” by getting wrapped in an effect. I think a better keyword would have been something like box, which conveys the act of packaging the value and is a common analogy in FP.

There’s also a section on “do notation” in chapter 4 of the book, but I (and others) found this pretty confusing on the first read though. Proposing we move this material on array comprehensions outside of the critical path for beginners.

4 Likes

I’m not sure I’d say it’s unfortunate that many resources expect you to have read the book; I think we should have resources that are aimed at intermediate or advanced users (in addition to resources which are suitable for beginners). Also, the book is just one path to becoming an intermediate user, but it’s definitely not the only one; if you’re coming from Haskell it’s probably mostly sufficient to just read through the Differences from Haskell page and maybe have a little trial and error to figure out how things work, and if you’re coming from Elm there are of course going to be some new concepts, such as type classes and higher kinded types, but you can probably still skip large chunks of the book.

I am happy to make the language reference more beginner-friendly to an extent, but only if that doesn’t get in the way of doing its primary job.

Maybe this symbol and keyword lookup can be built into Pursuit.

This sounds like a great idea to me :+1:

Also, for what it’s worth, pure is not a keyword, it’s an identifier (a function/value name); keywords are things like data or module which are part of the language syntax and not allowed as identifiers. I don’t think we can justify renaming pure at this point, although I tend to agree that it might not have been the best choice.

2 Likes

The thing about saying “monads are for effects” as that video does in the first few seconds is it can really throw you off as a beginner if you then look at, for example, list comprehensions or a do-block for a Maybe. For example, if you are in your first attempt to get extend main from log "🍝" to something else.

I really like your table there and the idea of adding to Pursuit.

A couple of simple diagrams, annotating a few uses for Monads (like Maybe, List, Effect) to spell out what do, <- and pure mean in each of those contexts would be very useful too. I will add an example here when i’m next at my iPad.

Actually, i think the meaning of let also needs to be spelt out in the do-block context (it’s hard to remember now, so great was my confusion when i first started)

A do-block for a Maybe is the core lesson of the video, so don’t think that will be confusing for beginners who watch it through.

I agree that these two terms can be confused though:

  • “effects” - includes Maybe
  • “native effects” (e.g. Effect)

For context on Maybe as an “effect”, here’s a quote from the original book:

As an example, the functor Maybe represents the side effect of possibly-missing values.


To help beginners who may find themselves on a page intended for advanced users, we should still include links back to the basic resources for context. This could be as lightweight as embedded wiki-style links.

Since Haskell also uses pure, I think it’s best to keep it as-is too. But perhaps add a note in the function reference about how it may be easier to think of it with a different name, and the reason for using the current name. This is the only other discussion I could find on naming. I like the suggestion of thinking of it as fromPure. Some examples of usage with common types would also improve clarity.

Yes, that’s exactly it - it’s really not (to me) obvious coming from non-FP programming that taking something out of a list is somehow an “effect”. It hit me like a tonne of bricks when i read that bit in Stephen Diehl’s excellent “What i wish…” page after i’d been banging my head against the wall for quite a while (this was five years ago, there was a lot less quality learning material for both Haskell and PureScript then).

I’m just going to add on to the existing conversation about the available learning resources with my own attempt answering your initial question, @Ding:

If you consider the randomInt function’s type signature:

randomInt :: Int -> Int -> Effect Int

…which, when you apply the two Int's per your code randomInt 1 6, what you now have is something with just the type signature randomInt 1 6 :: Effect Int.

So when you use it within the where block, you are not actually generating the “result”. Rather, you are giving a new name to a function that, each time you call it, will execute its action again and generate a new random number between 1 and 6. I’m probably going to make this distinction between naming and executing a few times :slight_smile:

So rather than result, this might be better named getNewResult or getNewInt:

getNewInt :: Effect Int
getNewInt = randomInt 1 6

Such that if you had the following main block of code, you should not see the same number logged three times (unless by bad luck!):

example1 :: Effect Unit
example1 = do
    result1 <- getNewInt
    result2 <- getNewInt
    result3 <- getNewInt
    log $ show result1
    log $ show result2
    log $ show result3
  where
    getNewInt :: Effect Int
    getNewInt = randomInt 1 6

Creating this getNewInt function could also be written within the do block using a let binding:

example2 :: Effect Unit
example2 = do
    let getNewInt :: Effect Int
        getNewInt = randomInt 1 6
    result1 <- getNewInt
    result2 <- getNewInt
    result3 <- getNewInt
    log $ show result1
    log $ show result2
    log $ show result3

In both the where and let cases, the use of the equals sign = can be thought of the same as “I’m giving the code randomInt 1 6 a new name that I can use in its place”, such that I could replace any of the subsequent getNewInt uses in the above block with randomInt 1 6 again and it would still mean the same thing. For example:

    ...
    result1 <- getNewInt
    result2 <- randomInt 1 6
    result3 <- getNewInt
    ...

Each of the times that you see <-, you can think of that as "I am executing the action on the right of the <- and binding its result to the name on the left of the <-. So to compare the difference again…

  • Using = in this context is for giving another reusable name on the left to the actions on the right
  • Using <- in this context is for executing the action on the right and storing its result in the name on the left.

As for why your use of show didn’t work in this context, it’s because what you were wanting to achieve is show :: Int -> String on the result, but you were not applying it to the restul. You were applying it to the action that (…when it gets executed) might create the result. That is, what you were trying to do is show :: Effect Int -> String.

You were close though! That is, you can achieve something like what you’re looking for by changing our getNewInt :: Effect Int function to a getNewString :: Effect String function.

That is, it will be an action that, each time it is run, it will generate a new Int between 1 and 6 and then turn that result into a String so that it can be logged to the console.

Without getting too into the details at this stage, what we need is a function that will turn our Effect Int into an Effect String. Or, more generally, an Effect a into any Effect b. This is where the map function comes into play. For our purposes, it will have the type signature:

map :: (Int -> String) -> Effect Int -> Effect String
--          [1]              [2]            [3]

You can read this as:

  1. “If you give me a function that turns an Int into a String…”
  2. “…and an Effect action that produces an Int…”
  3. "…then I can can give you back an Effect action that will produce a value of type String"
    And we already know that show will be our function that can turn an (Int -> String), so we could write this all with a where block outside the do as:
example3 :: Effect Unit
example3 = do
    result1 <- getNewString
    result2 <- getNewString
    result3 <- getNewString
    
    log $ result1
    log $ result2
    log $ result3
  where
    getNewInt :: Effect Int
    getNewInt = randomInt 1 6
    
    getNewString :: Effect String
    getNewString = map show getNewInt

…or with a let block inside the do again as…

example4 :: Effect Unit
example4 = do
    let getNewInt :: Effect Int
        getNewInt = randomInt 1 6        
    
        getNewString :: Effect String
        getNewString = map show getNewInt
        
    result1 <- getNewString
    result2 <- getNewString
    result3 <- getNewString
    
    log $ result1
    log $ result2
    log $ result3

And remember, in those two above examples, our use of = means we are just giving the new name getNewString to the actions on the right. So in that last block of actions where we executed it 3 times…

     ...
    result1 <- getNewString
    result2 <- getNewString
    result3 <- getNewString
    ...

…we could instead replace some of those calls with the definition again like we did earlier. That is, the below is the same as the above:

     ...
    result1 <- getNewString
    result2 <- map show getNewInt
    result3 <- map show (randomInt 1 6)
    ...

All three actions executed above are the same. The only difference here is whether we are using the underlying definition directly (such as in map show (randomInt 1 6) or whether we are using our own named functions that factor out part or all of that (like getNewInt and getNewString do). And, of course, if you dig down even further, you’d find that even randomInt is just a helpful name for another set of actions. And we could replace randomInt 1 6 with whatever longer code is its definition.

So considering your own code from the start:

  ...
  where
    result = (show (randomInt 1 6))

…you were very close! All you needed was a map to have a valid action there:

  ...
  where
    getNewString :: Effect String
    getNewString = map show (randomInt 1 6)

Just remember. That’s just us giving a name to the action we might want to re-use, and not actually executing the action.
There’s also nothing that says we have to define these as local functions within a let or where block. We could make them top-level functions so that we can use getNewString all over our code-base in separate module files. For example:


-- | An example top-level version of the function
getNewString_v1 :: Effect String
getNewString_v1 = map show (randomInt 1 6)

-- | This time using `(<$>)`, which
-- | is the infix version of `map`
getNewString_v2 :: Effect String
getNewString_v2 = show <$> randomInt 1 6

-- | This time with a more complicated String output:
getNewString_v3 :: Effect String
getNewString_v3 = do
  int <- randomInt 1 6
  pure ("Your result was: " <> show int)
  
-- | Same as above, but with `map` again
getNewString_v4 :: Effect String
getNewString_v4 = map displayResult (randomInt 1 6)
  where
    displayResult :: Int -> String
    displayResult i = "Your result was: " <> show i

-- | Same as above, but with `(<$>)`
getNewString_v5 :: Effect String
getNewString_v5 = displayResult <$> randomInt 1 6
  where
    displayResult :: Int -> String
    displayResult i = "Your result was: " <> show i
    
example5 :: Effect Unit
example5 = do
        
    result1 <- getNewString_v1
    result2 <- getNewString_v2
    result3 <- getNewString_v3
    result4 <- getNewString_v4
    result5 <- getNewString_v5
    
    traverse_ log [result1, result2, result3, result4, result5]

Without getting into the relationship between Functor and map (…or <- and (>>=) with Monad), I’d say the takeaway out of all this is again the heuristic that:

  • Using = with let/where is about naming the action that you define on the right. This is the same as the implication of = in a top-level function in your files.
    • e.g. functionName = someAction arg1 arg2
  • Using <- within a do block is about executing those actions, where any result is then stored in whatever label you put on the left of the <-
    • e.g. resultOfExecutingAction <- functionName

I’ve put all of the above code in a Gist that you can view in action on TryPureScript here:
https://try.purescript.org/?gist=1e934a7d0e2e36389a77f74c242203dd

Happy to answer any additional questions. Also happy to accept any corrections or feedback from anyone (…such as I’ve been imprecise/general/counterproductive in my attempt at being beginner friendly) :slight_smile:

3 Likes

@and-pete
Thanks for your gorgeous explanation! I’m still confused about Effect , do , monad, type/variable namingspaces, and many other concepts, but I can understand your code! I can see the logic through it.

The most significant things I learnt are make a function and execute a function :

  • Using = with let / where is about naming the action that you define on the right. This is the same as the implication of = in a top-level function in your files.
    • e.g. functionName = someAction arg1 arg2
  • Using <- within a do block is about executing those actions, where any result is then stored in whatever label you put on the left of the <-
    • e.g. resultOfExecutingAction <- functionName

I have loads of materials to digest(thanks to everyone on this forum helped me a lot :grin:), I’m trying to read some Haskell tutorials to understand those “rune symbols” (e.g. >>= ) and start over to read the purescript book .

1 Like