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
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:
- “If you give me a function that turns an
Int
into a String
…”
- “…and an
Effect
action that produces an Int
…”
- "…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)