Having Trouble Getting Started With PureScript

I’ve been working with PureScript for a little over a week now and basic things are still causing problem for me, such as setting up a new project and implementing a simple, working Main.purs that compiles. I found a great guide on this forum that describes the best tooling to use as of 2019. Following this document, I am now doing the following to set up:

yarn spago init
yarn spago run
yarn spago install maybe
yarn spago install lists
yarn spago install strings
yarn spago build

So this works ok, until I actually try to write code in VS Code. A simple example:

module Main where

import Prelude
import Effect (Effect)
import Effect.Console (log)
import Data.Maybe
import Data.List as List
import Data.String as String

main :: Effect Unit
main = do
  isPangram "Just a string."
  log "🍝"

isPangram :: Maybe String -> Boolean
isPangram Nothing = false
isPangram string =
  let 
    alpha_list = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']
  in
    innerPangram fromMaybe string alpha_list 0

innerPangram :: Maybe String -> List -> Int -> Boolean
innerPangram    _         []           _   = true
innerPangram    input     list         cur = 
  let
    lower_input = String.toLower input
  in
    if List.elemIndex input[cur] list then
      true
    -- if Set.empty the_set 
    -- if not Set.empty alpha_set cur then
      -- if Set.member then
        -- alpha_set = Set.delete alpha_set lower_input[cur]
    else
      innerPangram input alpha_set

I then attempt to run yarn spago build and start getting unexpected errors, like:

yarn run v1.22.4
warning package.json: No license field
$ /home/me/Documents/projects/codes/purescript/playground/node_modules/.bin/spago build
[info] Installation complete.
Compiling Main
Error found:
in module Main
at src/Main.purs:24:33 - 24:37 (line 24, column 33 - line 24, column 37)

Unknown type List

See https://github.com/purescript/documentation/blob/master/errors/UnknownName.md for more information,
or to contribute content related to this error.

[error] Failed to build.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

As far as I can tell, I’m following the docs, but I just keep getting problems like this and it’s super frustrating.

Could someone please tell me what I am doing wrong? :smiley: Thanks.

There’s actually quite a lot of problems with this code as it currently stands. While I could go through each one, I don’t know how helpful that would be to you. I’ll still go through some, but please don’t be discouraged by it. I can tell that there is a lot you’re not yet understanding.

In case you haven’t heard of it, I’d recommend you check out my learning repository, which has a clear but verbose bottom-up approach to teaching PureScript: https://github.com/jordanmartinez/purescript-jordans-reference For your situation, I would recommend reading through the Syntax folder here as a bare minimum: https://github.com/JordanMartinez/purescript-jordans-reference/tree/latestRelease/11-Syntax

I’d also recommend using the REPL to get used to PureScript syntax before you start writing programs in it. That will avoid some of the problems you’re experiencing above. To start that, you would run yarn spago repl.

As for the actual error message, let me explain it (though there’s about 10 other ones in here that you just haven’t seen yet because the compiler stops at the first one). List is a type you haven’t actually imported. While you wrote import Data.List as List, you’re said, "Make anything specified in the module, ‘Data.List’, available, so that I can refer to it via List.value or List.function or List.Type. What you actually want is this:

import Data.List as List -- so your other code still works
import Data.List (List(..)) -- import the List type directly

The above syntax’s second line says, "From the Data.List module, import the type, List, and all of its data constructors (e.g. Cons and Nil).

Second problem. You defined innerPangram's type signature so that it takes a List as its second argument. But what does that List store? Is it a List Int, a list of integers? Or a List String a list of Strings? You need to add another type to that definition.

Third problem, you’re using Array syntax (e.g. [], an empty array) in a pattern match when referring to a List _ type. Looking at the rest of your code, it seems you actually want an Array String, not a List. So, you would want to write this:

innerPangram :: Maybe String -> Array String -> Int -> Boolean
innerPangram    _         []           _   = true
innerPangram    input     list         cur = 
  -- rest of the code...

Fourth problem. PureScript does not have array-index syntax (e.g. arrayName[4] == "fourth element"). You would have to use unsafePartial (unsafeIndex arrayName 4) to get the fourth element in an array. Since that function is not a “total” function (e.g. the function will throw a runtime error on some inputs, such as an empty array), you must also use unsafePartial. There’s a better way to achieve the same idea but without risking the possibility of getting a compiler error. Typically, we would use Data.Array.index, which returns a Maybe a:

case Data.Array.index array cur of
        Just val -> val == lower_input
        Nothing -> innerPangram input alpha_set

Fifth problem. What is alpha_set? Since you never specify what that binding refers to, you would eventually get it as a compiler error.

Sixth problem. innerPangram takes a Maybe String argument that you refer to via the binding, input. You don’t expose the value of the Maybe type (e.g. Nothing or Just string) when pattern matching on it. Right now, you are creating a binding to that value. Thus, String.toLower input will produce a compiler error. You need something like this:

innerPangram :: Maybe String -> Array Int -> Int = Boolean
innerPangram _ [] _ = true
innerPangram input array cur =
  case input of
    Nothing -> -- what do you do if there isn't a String value?
    Just string ->
      let lower_input = String.toLower string
      in case Array.index array cur of
        Just val -> val == lower_input
        Nothing -> innerPangram input alpha_set

Seventh problem and I’ll stop with this one. In main you are using “do notation.” Since main has the Effect monad as its type signature and you’re immediately using do notation there, then every line in that function must produce an Effect returnValue. isPangram returns a Boolean, not Effect _. Thus, there’s a compiler error, too. It would be better to use a let binding:

main :: Effect Unit
main = do
  let result = isPangram "Just a string."
  log "🍝"
1 Like

Oof! Thanks for the very thorough response. There is a lot of inconsistency in the code, partly because I was trying different things. I’ve read a lot of the learning repository and I came away with the impression that mostly people use Array in PureScript, not List? In any event, I should have cleaned this up better before asking for help. I started out trying to use Set but that didn’t go so well. Tried using List instead. Was actually using Array syntax. Oy vey.

Hi @daveman1010221 you can also try Slack for help and just #purescript and #purescript-beginners https://fpchat-invite.herokuapp.com/ There a lot of people are active too.

I made a PDF from the purescript book and also a PDF of the halogen documentation. Which is not available directly (i generated it from the markdown in the github repository). I can send you these.

By the way if .. else .. then is only for producing a value. If you want to express do this if/when condition then use when. Since function, when, is not used as much as you would think. Just saying because i saw a lot of if in your code without else. You can use it while in monad (do-notation).

import Control.Monad (when)

main = do
  when (2 < 3) $ log "2 is smaller than 3"

In your case though, probably a lot of those ifyou will end up using let to make some value that in itself uses if .. else .. then. But i can’t give an example because i’m not sure what the code is supposed to do.

1 Like

There is a lot of inconsistency in the code, partly because I was trying different things.

Ah, that makes more sense.

I came away with the impression that mostly people use Array in PureScript, not List?

I’d say that’s probably accurate. If you need something to be performant, Array is the better choice for most situations. However, if you have a problem that is best solved via a linked list, then List would be the better option. In FP, we would use what’s called a FingerTree to get a performant but still FP-friendly data structure, but it’s not very performant due to lack of compiler optimizations: purescript-sequences.

1 Like

List has better asymptotics for lots of operations, e.g. cons and uncons are O(1) on List but O(n) on Array. However Array ends up winning in many real-world situations, just by virtue of being implemented in C/C++ within the JS engine.

2 Likes

I did eventually get this code to work, it seems I learned a lot along the way. However, I’m not sure I’m doing some things the cleanest way, it’s just working with what I know. In case anyone wanted to see that:

    module Pangram
  (
    isPangram
  ) where

import Prelude
import Data.Array as Array
import Data.List as List
import Data.List (List)
import Data.Maybe (Maybe(..))
import Data.String as String
import Data.String.CodePoints as CP

isPangram :: Maybe String -> Boolean
isPangram string = 
  let 
    pan_list = List.fromFoldable (CP.toCodePointArray "abcdefghijklmnopqrstuvwxyz")
  in
    case string of
      Nothing ->
        false
      Just a_string ->
        if String.length a_string < List.length pan_list then
          false
        else
          innerPangram (String.toCodePointArray(String.toLower a_string)) (Just pan_list) 0


innerPangram :: (Array CP.CodePoint) -> Maybe (List CP.CodePoint) -> Int -> Boolean
innerPangram _     Nothing           cur = true
innerPangram input (Just pan_list) cur = 
  if List.null pan_list then
    true
  else
    let
      currentCodePoint = Array.index input cur
    in
      case currentCodePoint of
        -- The nothing case is impossible.
        Nothing -> false
        Just the_codepoint ->
          let
            list_idx = List.elemIndex the_codepoint pan_list
          in
            case list_idx of
              Just val ->
                innerPangram input (List.deleteAt val pan_list) (cur + 1)
              Nothing ->
                innerPangram input (Just pan_list) (cur + 1)

Here’s some thoughts…

isPangram :: Maybe String -> Boolean
isPangram = case _ of
  -- through the 'case _ of' approach, you never calculate the 'pan_list'
  -- if it isn't needed due to hitting the Nothing branch
  Nothing -> false
  -- no need for 'a_string' binding now, 
  -- leading to more readable code
  Just string ->
    let 
      pan_list = List.fromFoldable (CP.toCodePointArray "abcdefghijklmnopqrstuvwxyz")
    in if String.length string < List.length pan_list then
          false
        else
          innerPangram (String.toCodePointArray (String.toLower string)) (Just pan_list) 0

Since pan_list is a constant value, it might be better to refactor that outside of the function

isPangram :: _
isPangram = case _ of
  -- and use it in your function here

pan_list :: _ -- type signature here
pan_list = List.fromFoldable (CP.toCodePointArray "abcdefghijklmnopqrstuvwxyz")

You could use pattern guards to simplify the logic

innerPangram :: _
innerPangram _ Nothing cur = true
innerPangram input (Just pan_list) cur
  | List.null pan_list = true
  | otherwise =
      let currentCodePoint = --
      -- rest of your code
1 Like

Thanks, Jordan, this is very helpful. I think I just need to read a lot more code. I saw that I didn’t need a few of those variables from looking through other people’s code a short while ago. With that said, I’m probably made a bit lazy from my tooling in other languages, which would have immediately caught some of these obvious patterns and suggested some re-factoring. VS Code is the best tool I’ve found for working with PureScript so far. I sort of gave up on using NeoVim after my experience trying to use that with Elm.

You start to pick up things like this over time.

Community solutions are one of my favorite features of exercism.io. It really helped me through the first FP learning curve with Elm. I tried it with PureScript, but the exercise ordering is common across all languages, so not in the most logical order for learning beginner PS concepts.

I’d like to get the PureScript book exercises on there, or at the very least, bring a similar testing environment to the book’s exercises.

3 Likes