My first Purescript project - help/input request

Hi

I mentioned to @thomashoneyman and @JordanMartinez I am working on a toy project as first intro to building something with Purescript.

The project is a refactor of a simple web app which frontend was written in Purescript Pux 1.0.0. My objective is to rewrite using current Purescript and Halogen. Here’s the repo https://github.com/p2327/sciQs and a wireframe:

As I am reading though the book and learn halogen / real world halogen I will update this post with updates/questions etc.

Thanks for reading!

1 Like

So I started working on the Purescript implementation. Code here.

My first objective is to get the JSON data served by the backend into the app.

I followed the purescript-foreign examples from @garyb

There’s a couple of things I want the code to do that I can’t get it to do, plus a couple of questions – I would appreciate some help so I can continue as a the moment I am stuck :slight_smile:

  • In my implementation the type Question should have a chosenAnswer field for , this is commeted out in the above. This field indicates which answer the player clicks, and unless the click event has registered it should be Nothing.
    • How do I get this Nothing into my readQuestion function? I tried fromMaybe or just readNullOrUndefined but I get a Maybe foreign return type…
-- Read JSON question into a Foreign type
readQuestion :: Foreign -> F Question
readQuestion value = do
  questionText  <- value ! "questionText"  >>= readString
  answers       <- value ! "answers"       >>= readArray >>= traverse readString
  correctAnswer <- value ! "correctAnswer" >>= readInt
  -- chosenAnswer  <- value ! ""              >>= fromMaybe readNullOrUndefined Nothing
  -- pure { questionText, answers, correctAnswer, chosenAnswer }
  pure { questionText, answers, correctAnswer }
  • For the moment all the main function is checking if I read the JSON ok.
    • I copied the Util/foreign value from the examples, is it required to read my json?
module Utils.Value where

import Prelude

import Data.Function.Uncurried (Fn3, runFn3)
import Foreign (F, Foreign, ForeignError(..), fail)

foreign import foreignValueImpl :: forall r. Fn3 (String -> r) (Foreign -> r) String r

foreignValue :: String -> F Foreign
foreignValue json = runFn3 foreignValueImpl (fail <<< ForeignError) pure json
"use strict";

exports.foreignValueImpl = function (left, right, str) {
  try {
    return right(JSON.parse(str));
  } catch (e) {
    return left(e.toString());
  }
};

I understand the JavaScript implementation handles the error on Left and parses the JSON on Right, but I am not sure this might be complicating things down the line when I need to access the Question type?

For example I will want to access the Question later to do something like

initialState :: State
initialState = { score: 0, questions: [], waitingForQuestion: true }

appendQuestion :: Question -> State -> State
appendQuestion question state =
  if state.waitingForQuestion
  then state { questions          = snoc state.questions question,
               waitingForQuestion = false }
  else state

Good idea with sharing your repo.
I made a PR with some suggestions here.
This might not have addressed all of your questions, so feel free to ask for any follow-up help.

1 Like

hey @milesfrain this is amazing, thanks so much - I really appreciate you doing this. I was getting a bit desperate trying to set up this project correctly! And thank god for the .gitignore, that was ugly :smiley:

Meanwhile I figured out I can pass a default value to my readQestion pure function to get what I envision…I don’t know if it is idiomatic at all however.

readQuestion :: Foreign -> F Question
readQuestion value = do
  questionText  <- value ! "questionText"  >>= readString
  answers       <- value ! "answers"       >>= readArray >>= traverse readString
  correctAnswer <- value ! "correctAnswer" >>= readInt
  pure { questionText, answers, correctAnswer, chosenAnswer: Nothing }

Which checked in repl

> import Sciqs
> main
(Right { answers: ["Answer1","Answer2","Answer3","Answer4"], 
         chosenAnswer: Nothing, 
         correctAnswer: 1, 
         questionText: "Sample Question" })
unit

That said, I will try your approach and follow up - thanks again for taking the time to look into this

@milesfrain accepted the PR and replied there cheers

Added some comments on your latest commit here.

@milesfrain hey you just made my day! I have replied to your comments. Thanks so much again for being so helpful! :slight_smile:

I am trying to add a color change on click and thought to do it with to change class via HE.input_

However I get this errors at compile

Error 1 of 2:

  in module Main
  at src\Main.purs:113:31 - 113:40 (line 113, column 31 - line 113, column 40)

    Unknown value HE.input_

Error 2 of 2:

  in module Main

    Unknown type H.ComponentDSL

This is the code

module Main where

import Prelude
import Affjax as AX
import Affjax.ResponseFormat as ResponseFormat
import Data.Array (mapWithIndex)
import Data.Either (Either(..))
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff.Class (class MonadAff, liftAff)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML (ClassName(..))
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Halogen.Themes.Bootstrap4 as B
import Halogen.VDom.Driver (runUI)
import Simple.JSON as SimpleJSON

-- Server url
questionServiceUrl :: String
questionServiceUrl = "http://localhost:8080/question"

-- Question type used for both JSON parsing and state
type Question
  = { questionText :: String
    , answers :: Array String
    , correctAnswer :: Int
    }

data Action
  = NewGame
  | ClickAnswer Int
  | NextQuestion
  | Toggle -- Added Toggle for handling button color change

data Status
  = WaitingForQuestion
  | HaveQuestion Question (Maybe Int)
  | Failure String

type State
  = { score  :: Int
    , status :: Status
    }

-- State of button
type ButtonState
  = Boolean

-- Query approach for button class change
data Query a
  = UpdateButtonState a
  | GetButtonState

{-
-- Change class of button to handle color
-- Maybe I can just handle changing mkButton?
toggleButtonClass :: forall m. ButtonState -> H.ComponentHTML Action () m
toggleButtonClass isClicked =
    let
      toggleLabel = if isClicked then "clickedButton" else "notClickedButton"
    in
      HH.button
        [ HP.class_ $ ClassName toggleLabel
        , HE.onClick \_ -> Just Toggle
        ]
-}

-- Start out with no questions.
initialState :: State
initialState = { score: 0, status: WaitingForQuestion }

-- Helper function for creating buttons that trigger an action
mkButton :: forall a. String -> Action -> HH.HTML a Action
mkButton str act =
  --let
    --toggleLabel = if isClicked then "clickedButton" else "notClickedButton"
  HH.button
    [ HP.classes [ B.btnLg, B.btnBlock ]
    , HE.onClick \_ -> Just act
    ]
    [ HH.text str ]

-- | Shows how to add event handling.
render :: forall m. State -> H.ComponentHTML Action () m
render s =
  let
    questionBlock = case s.status of
      WaitingForQuestion -> HH.text "Loading..."
      HaveQuestion question maybeAnswer ->
        let
          answerSummary = case maybeAnswer of
            Nothing -> []
            Just idx ->
              [ HH.div_
                  [ HH.text
                      $ if idx == question.correctAnswer then
                          "Correct"
                        else
                          "Incorrect"
                  ]
              ]
        in
          HH.div_
            $ [ HH.text question.questionText
              ]
            <> mapWithIndex (\idx txt -> HH.div_ [ mkButton txt $ ClickAnswer idx ]) question.answers
            <> answerSummary
          -- Adding input and query handler to change CSS class
              [ HH.button
                [ HE.onClick (HE.input_ UpdateButtonState)
                , HP.classes $ map className 
                  [ if true then "clickedButton" else "notClickedButton"
                  ] 
                ]
              ]
      Failure str -> HH.text $ "Failed: " <> str
  in
    HH.div [ HP.classes [ B.containerFluid ] ]
      [ HH.div [ HP.classes [ B.h1 ] ] [ HH.text "SciQs" ]
      , HH.div_ [ mkButton "New Game" NewGame ]
      , HH.div_ [ mkButton "Next Question" NextQuestion ]
      , HH.div_ [ HH.text $ "Score: " <> show s.score ]
      , questionBlock
      ]

-- Evaluate input query
eval :: Query ~> H.ComponentDSL ButtonState Query Void m
eval (UpdateButtonState next) = do
  H.modify_ not
  pure next
eval (GetButtonState reply) = do
    reply <$> H.get

-- | Shows how to use actions to update the component's state
handleAction :: forall o m. MonadAff m => Action -> H.HalogenM State Action () o m Unit
handleAction = case _ of
  NewGame -> do
    H.modify_ \s -> s { score = 0 }
    handleAction NextQuestion
  ClickAnswer idx ->
    H.modify_ \s -> case s.status of
      HaveQuestion q _ ->
        let
          points = if idx == q.correctAnswer then 1 else 0
        in
          s { status = HaveQuestion q (Just idx), score = s.score + points }
      _ -> s { status = Failure $ "Somehow clicked idx " <> show idx <> " when not in question display state" }
    -- Add action to change button color
    H.modify_ \s -> case s.action of
      Toggle -> do
        H.modify_ \oldState -> not oldState
  NextQuestion -> do
    H.modify_ \s -> s { status = WaitingForQuestion }
    result <- liftAff $ AX.get ResponseFormat.string questionServiceUrl
    case result of
      Left err -> H.modify_ \s -> s { status = Failure $ "GET /api response failed to decode: " <> AX.printError err }
      Right response -> case SimpleJSON.readJSON response.body of
        Right (r :: Question) -> do
          H.modify_ \s -> s { status = HaveQuestion r Nothing }
        Left e -> H.modify_ \s -> s { status = Failure $ "Can't parse JSON. " <> show e }

component :: forall q i o m. MonadAff m => H.Component HH.HTML q i o m
component =
  H.mkComponent
    { initialState: const initialState
    , render
    , eval:
        H.mkEval
          $ H.defaultEval
              { handleAction = handleAction
              , initialize = Just NewGame
              }
    }

main :: Effect Unit
main =
  HA.runHalogenAff do
    body <- HA.awaitBody
    runUI component unit body

The ComponentDSL type is from Halogen 4 and was a type synonym for HalogenM; it doesn’t exist in Halogen 5 and you’d just use the HalogenM type directly.

It looks like your eval function is written in the Halogen 4 style, but in your case you probably want to rename it handleQuery, add it to your component’s eval spec (the same place where you provided handleAction to your component), and change the type signature to be HalogenM.

1 Like

After a lot of help from Miles and Thomas I was able to get the app running :slight_smile:

You can play the game here
http://reandomsience.s3-website.eu-west-3.amazonaws.com/

I have added quite a bit of comments to the repo, a sense check and further recommendations to the code structure are welcome!

I’ll be updating this post once I have completed a short write up so that other people can replicate / expand upon the project with ease

Thanks again!

2 Likes