How to write the condition type?

Consider a game. There are two basic data structures: players and enemy.

type Player
  = { name :: String
    , hp :: Number
    , atk :: Number
    , speed :: Number
    }

type Enemy
  = { name :: String
    , hp :: Number
    , atk :: Number
    , speed :: Number
    }

Now I want a structure description command, for example

["Player", "hp", 100]

Means to set the player’s health to 100.

So i wrote this:

data Cmd = String String Number

But this is not good enough, I can enter anything, like:

Cmd "aaa" "bbb" 1

It doesn’t make sense.

After thinking about it I wrote this:

data ModuleNmae = Player | Enemy
data Cmd = Cmd ModuleNmae String Number

Looks better, but I can still write:

Cmd Player "aaa" 1

Can we use another type to describe a type?

I hope that the type of the second parameter is determined by the first parameter, and the type of the third parameter is determined by the second parameter.

As a comparison, you can write like this in typescript:

type Player = {
    name: string
    hp: number
    atk: number
    speed: number
}
type Enemy = {
    name: string
    hp: number
    atk: number
    speed: number
}
type Index = {
    Player: Player
    Enemy: Enemy
}

function genCmd<M extends keyof Index, K extends keyof Index[M], V extends Index[M][K]>(
    moduleName: M,
    key: K,
    value: V,
) {}

var c1 = genCmd('Player', 'hp', 1) // ok
var c2 = genCmd('Player', 'aaa', 1) // error
var c3 = genCmd('Player', 'hp', "aaa") // error
var c4 = genCmd('aaa', 'hp', 1) // error
var c5 = genCmd('Player', 'name', 1) // error

Or the idea itself is wrong, should the logic be described in other ways?

1 Like

It depends on how would you like to use this Cmd type. Is it going to be used to update record’s fields? If so, I would create a type like

data Cmd a b = Cmd (b -> a -> a) b

apply :: forall a b. Cmd a b -> a -> a
apply (Cmd f v) x = f v x

and for this type I’d create a smart constructor to work on record fields, along the lines of

mkCmd :: forall lab b r rt
  .  Cons lab b rt r 
  => IsSymbol lab 
  => Proxy lab 
  -> Proxy r 
  -> b 
  -> Cmd (Record r) b
mkCmd p rp val = Cmd (set p) val

(perhaps the Proxy r is redundant here).

2 Likes

first of all, I think this is an over-abstraction, I think the idea that we can send a command with any field and type is simply too much, think about it this way after some time maybe you want to create a field inside enemy and player but you don’t want to create command for them.
but here is the code in purscript

module Main where

import Prelude

import Data.Symbol (class IsSymbol)
import Prim.Row (class Cons)
import Type.Proxy (Proxy(..))
import Unsafe.Coerce (unsafeCoerce)

type PlayerRow
  =
  ( name :: String
  , hp :: Number
  , atk :: Number
  , speed :: Number
  )

type EnemyRow
  =
  ( name :: String
  , hp :: Number
  , atk :: Number
  , speed :: Number
  , foo :: Array Number
  )

type IndexRow =
  ( player :: PlayerRow
  , enemy :: EnemyRow
  )

mkCmd
  :: forall indexField (indexType :: Row Type) indexTail field fieldType fieldTail
   . IsSymbol field
  => IsSymbol indexField
  => Cons indexField indexType indexTail IndexRow
  => Cons field fieldType fieldTail indexType
  => Proxy indexField
  -> Proxy field
  -> fieldType
  -> Record (indexType)
mkCmd index field value = unsafeCoerce 0

c1 = mkCmd (Proxy :: Proxy "player") (Proxy :: Proxy "name") "player"

c2 = mkCmd (Proxy :: Proxy "enemy") (Proxy :: Proxy "foo") [ 1.0, 2.0, 3.0 ]

I know that the ergonomics is not like typescript but let me explain what is happening here.

I will explain what Proxy is, in purescript we cannot simply go from a string to a type string in typescript you can easily say this string is a constant and use it in typelevel here if we want to upgrade a term level(something that is working and inside js) to a type level, you should use Proxy, the Proxy is a Phantom Type you can search about it more and finds out why it’s usefull in other contexts too.

if you look at mkCmd I used two type class IsSymbol and Cons the IsSymbol is easy you constraint a type that should be Symbol when creating RowType the label should be Symbol
this is a rowType ( name :: String) here name is a label and String is its type pay attention that RowType is not Record yet you can create a Record using Record (name:: String).

and about Cons this typeclass is interesting it accepts 4 type

class Cons (label :: Symbol) (a :: k) (tail :: Row k) (row :: Row k) | label a tail -> row, label row -> a tail

it says if you give me a label and a type called a and a RowType called tail I will add this label and its type to the tail and give you a row
label a tail -> row
but it can do another thing, it says if you give me a label and row I will give you the type of label inside row a and also all other row types in it called tail

label row -> a tail

in mkCmd we did not use the tails but we should define it in forall

I created a gist for you to play around

https://try.purescript.org/?gist=f9965e5d3ae3d669d7aa279e1976b74b

but if you explain what you wish to accomplish, maybe we can give you a better Idea and solution

1 Like

Thank you very much for your code. After reading it, I checked the relevant information, including Cons, Proxy and Symbol. I now understand how to make type constraints in purescript.

In addition, this code has a small problem, it should be Cmd (Record rt) b instead of Cmd (Record r) b.

Thank you very much for your examples and explanations.

I have only seen PureScript by Example before, There is no mention of Proxy there, I saw @Swordlash code just now, I spent a lot of time reviewing documents on Pursuit, studying the code in the code base. Now I understand how to use it. Thank you for explaining to me again.

I noticed that there are many class in Pursuit that I haven’t seen before. Are there any books or materials that introduce more usage of class?

Here I want to make a simple idle game. The minimum process is: every second, the player attacks the enemy once, the enemy attacks the player once, and a prompt message is printed on the screen. If the player dies, the player’s hp is set to 100, and if the enemy dies, the enemy’s hp is set to 100. I have written the display code in JavaScript, and I want to use purescript to implement the combat part.

I think the fp-style program should be a value-to-value mapping. Here, when JavaScript calls purescript, enter the relevant data of the player and the enemy, and then purescript maps to the Cmd array and returns it to JavaScript. The JavaScript sets these values according to the command, updates the display, and starts a new loop.

I thought that the context of Either could express conditional judgment, so I wrote:

module Game where

import Prelude
import Data.Either (Either(..))

data Cmd
  = Cmd String String Number
  | Nil

type Player
  = { name :: String
    , hp :: Number
    , atk :: Number
    , speed :: Number
    }

type Enemy
  = { name :: String
    , hp :: Number
    , atk :: Number
    , speed :: Number
    }

checkPalyer :: Player -> Either (Array Cmd) (Array Cmd)
checkPalyer p
  | _.hp p <= 0.0 = Left [ (Cmd "player" "hp" 100.0) ]
  | otherwise = Right [ (Nil) ]

checkEnemy :: Enemy -> Either (Array Cmd) (Array Cmd)
checkEnemy e
  | _.hp e <= 0.0 = Left [ (Cmd "enemy" "hp" 100.0) ]
  | otherwise = Right [ (Nil) ]

attack :: Player -> Enemy -> Either (Array Cmd) (Array Cmd)
attack p e = Right [ (Cmd "enemy" "hp" (_.hp e - _.atk p)), (Cmd "player" "hp" (_.hp p - _.atk e)) ]

main :: Player -> Enemy -> Array Cmd
main p e = case checkPalyer p <> checkEnemy e <> attack p e of
  Left a -> a
  Right b -> b

In this way, the conditional judgment is hidden in Either, the flow of the program becomes linear, Either will automatically handle the divergence, and the input and output of each function are simple, which is convenient for debugging.

But I was worried about spelling mistakes when I created Cmd, so I asked this question.

I’m not sure if this is reasonable, This is the first time I have written a practical fp program, is there a better way?

Yeah, I mistakenly swapped places of rt and r. rt was meant to be “record’s tail”, i.e. r without lab component. I edited the original post.

1 Like

if you want to know more about PhantomType you can read here Domain-Specific Languages - PureScript by Example
you can also read Jordan refresnce
Type Level Programming - PureScript: Jordan's Reference
I think Jordan Reference are great for learning after purescript book by example

also, I think in this book contains some infromation

I learn about rows and these classes from
https://purescript-simple-json.readthedocs.io/en/latest/intro.html
and

about your problem, I think you should not think about js when creating your core domain first
so don’t think about how you should call from js or send data to js first think about your Domain if you don’t know what I am talking about you can read Domain Modeling Made Functional book or at least watch this youtube video


module Game where

import Prelude

import Data.Function (applyFlipped)

infixl 1 applyFlipped as |>

data Cmd = SetPlayerHP Number | SetEnemyHP Number

newtype Player = Player
  { name :: String
  , hp :: Number
  , atk :: Number
  , speed :: Number
  }

newtype Enemy = Enemy
  { name :: String
  , hp :: Number
  , atk :: Number
  , speed :: Number
  }

combat :: Player -> Enemy -> Array Cmd
combat (Player { hp: 0.0 }) (_) = [ SetPlayerHP 100.0 ]
combat (_) (Enemy { hp: 0.0 }) = [ SetEnemyHP 100.0 ]
combat (Player { hp: playerHP, atk: playerAtk }) (Enemy { hp: enemyHP, atk: enemyAtk }) = [ SetEnemyHP (enemyHP - playerAtk), SetPlayerHP (playerHP - enemyAtk) ]

type CmdForJs =
  { type :: String
  , field :: String
  , value :: Number
  }

convert :: Cmd -> CmdForJs
convert (SetPlayerHP n) = { type: "player", field: "hp", value: n }
convert (SetEnemyHP n) = { type: "enemy", field: "hp", value: n }

main ∷ Player → Enemy → Array CmdForJs
main p e = combat p e |> map convert

you can see here that I say ok we have only to command setPlayerHP and setEnemyHP this way we limit ourself this way we can know for sure that what commands we exports
I changed type to newtype this way we cannot send an Enemy to a function that needs Player
and also I used a simple Pattern Match here this way I think is easier to read and think about
you can see that in the last line I changed the Cmd to CmdForJs here we can simply set our command that js needs

but there is a subtle bug here if enemy hp and player hp becomes less than 0 they don’t set to 100 anymore
now we should think about it this way is it even ok that hp became less than 0 or more than 100 ?

so here we can use Smart Constructors to create a newtype, smart constructor is like data type that you can not create outside the module cause the constructor is hidden.

module HP
  ( HP
  , dead
  , mk
  , attack
  , toNumber
  ) where

import Prelude

newtype HP = HP Number

derive newtype instance Eq HP

mk :: Number -> HP
mk a
  | a < 0.0 = HP 0.0
  | a > 100.0 = HP 100.0
  | otherwise = HP a

attack :: HP -> Number -> HP
attack (HP a) atk = mk $ a - atk

dead :: HP
dead = HP 0.0

toNumber :: HP -> Number
toNumber (HP a) = a

here we can see that I exported only the HP type if I wanted to export it with constructors I simply write HP(..)

and now our main code can be used this HP

module Game where

import Prelude

import Data.Function (applyFlipped)
import HP (HP)
import HP as HP

infixl 1 applyFlipped as |>

data Cmd = SetPlayerHP HP | SetEnemyHP HP

newtype Player = Player
  { name :: String
  , hp :: HP
  , atk :: Number
  , speed :: Number
  }

newtype Enemy = Enemy
  { name :: String
  , hp :: HP
  , atk :: Number
  , speed :: Number
  }

combat :: Player -> Enemy -> Array Cmd
combat (Player { hp }) (_) | hp == HP.dead = [ SetPlayerHP (HP.mk 100.0) ]
combat (_) (Enemy { hp }) | hp == HP.dead = [ SetEnemyHP (HP.mk 100.0) ]
combat (Player { hp: playerHP, atk: playerAtk }) (Enemy { hp: enemyHP, atk: enemyAtk }) = [ SetEnemyHP (HP.attack enemyHP playerAtk), SetPlayerHP (HP.attack playerHP enemyAtk) ]

type CmdForJs =
  { type :: String
  , field :: String
  , value :: Number
  }

convert :: Cmd -> CmdForJs
convert (SetPlayerHP n) = { type: "player", field: "hp", value: HP.toNumber n }
convert (SetEnemyHP n) = { type: "enemy", field: "hp", value: HP.toNumber n }

main ∷ Player → Enemy → Array CmdForJs
main p e = combat p e |> map convert


2 Likes

Wow, this one is very interesting.

Thank you for your reference, I will read it.

I watched the video and your code. Use data types to describe business logic and constraints. It looks good, more natural and simpler than my idea of imposing constraints.

I will try to write some programs in this way.

thank you very much.

2 Likes