Specifically, Felix made the goal of trying to update the given sample "Conway's Game of Life" program (about 450 lines of code) to support random initialization of the board. He felt this would give him a feel of how difficult it is to work in an environment with "uniqueness types" (aka "affine" or (incorrectly) "linear" types).
Felix did accomplish this goal, with about six or seven hours of effort (with no prior knowledge of Clean's syntax or semantics, though he did have experience with Haskell). Full source is available at the bottom of the page.
This is his story.
randomCell :: ClickPoint Life Int Int -> Life
randomCell origin state=:{gen,size,seed} xbound ybound
# (x,seed) = random seed
# (y,seed) = random seed
# cell = makeLifeCell {origin & x=origin.x + (x rem xbound), y=origin.y + (y rem ybound)} size
# state=:{gen} = {state & gen=insertCell cell gen, seed=seed}
= state
randomCells :: ClickPoint Life Int -> Life
randomCells origin state count
| count <= 0 = state
| otherwise = randomCells origin (randomCell origin state 500 500) (count - 1)
// randomC sets the current generation to empty, clears the window, and then initializes the window with some "random" state.
randomC :: (PSt Life) -> PSt Life
randomC life=:{ls=state=:{gen,size},io}
# state=:{seed} = {state & gen=makeGeneration}
# io = setWindowLook windowID True (True,look state) io
# (viewframe, io) = getWindowViewFrame windowID io
origin = viewframe.corner1
# state=:{gen} = randomCells origin state 1000
# io = setWindowLook windowID True (True,look state) io
= {life & ls=state, io=io}
randomC :: (PSt Life) -> PSt Life
randomC life=:{ls=state=:{gen,size},io}
# state=:{seed} = {state & gen=makeGeneration}
# io = setWindowLook windowID True (True,look state) io
# (viewframe, io) = getWindowViewFrame windowID io
origin = viewframe.corner1
# (x,seed) = random seed
# cell = makeLifeCell {origin & x=origin.x + (x rem 1000)} size
# state = {state & gen=insertCell cell gen, seed=seed}
# (x,seed) = random seed
# cell = makeLifeCell {origin & x=origin.x + (x rem 1000)} size
# state = {state & gen=insertCell cell gen, seed=seed}
# (x,seed) = random seed
# cell = makeLifeCell {origin & x=origin.x + (x rem 1000)} size
# state = {state & gen=insertCell cell gen, seed=seed}
# io = setWindowLook windowID True (True,look state) io
= {life & ls=state, io=io}
*PSt Life -> *PSt Life, and I was also importing the standard library function "random" which produces a tuple (Int, RandomSeed).random:random :: (PSt Life) -> PSt Life
random life=:{ls=state=:{size},io}
# state = {state & gen=makeGeneration}
# io = setWindowLook windowID True (True,look state) io
# (seed,life) = getNewRandomSeed {life & ls=state, io=io}
# ((xbig,seed),life) = (random(seed),life)
# ((ybig,seed),life) = (random(seed),life)
# (pos,life) = ({x=xbig mod 10,y=ybig mod 10},life)
= life
// The window mouse action places and removes alive cells:
track :: MouseState (PSt Life) -> PSt Life
track mouse life=:{ls=state=:{gen,size},io}
| modifiers.commandDown
# state = {state & gen=removeCell cell gen}
# io = appWindowPicture windowID (eraseCell size cell) io
# io = setWindowLook windowID False (True,look state) io
= {life & ls=state, io=io}
| otherwise
# state = {state & gen=insertCell cell gen}
# io = appWindowPicture windowID (drawCell size cell) io
# io = setWindowLook windowID False (True,look state) io
= {life & ls=state, io=io}
where
(pos,modifiers) = case mouse of
(MouseDown pos mods _) -> (pos,mods)
(MouseDrag pos mods) -> (pos,mods)
cell = makeLifeCell pos size
random, which is just a copy-and-paste from the definition of erase:// random sets the current generation to empty, clears the window, and then initializes the window with some "random" state.
random :: (PSt Life) -> PSt Life
random life=:{ls=state,io}
# state = {state & gen=makeGeneration}
# io = setWindowLook windowID True (True,look state) io
= {life & ls=state, io=io}
:+: MenuItem "&Random Cells (FSK)" [MenuId randomID, MenuShortKey 'r', MenuFunction (noLS random)]
:+:? What's noLS? Search me. I'm going to just click "go" and see what the type checker tells me.
module LifeGameExample
// **************************************************************************************************
//
// This is the LifeGame program.
//
// The program has been written in Clean 2.0 and uses the Clean Standard Object I/O library 1.2.2
//
// **************************************************************************************************
import StdEnv, StdIO
import Life, Help
import Random
:: Life
= { gen :: !Generation
, size:: !CellSize
, seed:: !RandomSeed
}
initialLife
= { gen = makeGeneration
, size= StartCellSize
, seed= nullRandomSeed
}
Start :: *World -> *World
Start world
= startLife (openIds 7 world)
startLife :: ([Id],*World) -> *World
startLife ([eraseID,playID,haltID,stepID,windowID,timerID,randomID],world)
# (newseed,world) = getNewRandomSeed world
# initialLife = {initialLife & seed=newseed }
= startIO SDI initialLife
initialise
[ProcessClose closeProcess]
world
where
// initialise creates the gui of the life.
initialise pst
# (error,pst) = openTimer undef timer pst
| error<>NoError
= abort "LifeGameExample could not open timer."
# (error,pst) = openMenu undef file pst
| error<>NoError
= abort "LifeGameExample could not open Life menu."
# (error,pst) = openMenu undef options pst
| error<>NoError
= abort "LifeGameExample could not open Options menu."
# (error,pst) = openMenu undef commands pst
| error<>NoError
= abort "LifeGameExample could not open Commands menu."
# (size, pst) = accPIO getProcessWindowSize pst
# (error,pst) = openWindow undef (window size) pst
| error<>NoError
= abort "LifeGameExample could not open Life window."
| otherwise
= pst
// window defines the window that displays the universe and its inhabitants.
window size = Window "Life" NilLS
[ WindowId windowID
, WindowClose (noLS closeProcess)
, WindowMouse onlyMouseDown Able (noLS1 track)
, WindowViewDomain (getViewDomain StartCellSize)
, WindowViewSize size
, WindowOrigin zero
, WindowHScroll (stdScrollFunction Horizontal StartCellSize)
, WindowVScroll (stdScrollFunction Vertical StartCellSize)
, WindowLook True (look initialLife)
, WindowPen [PenBack Black]
]
// timer defines the timer that calculates subsequent life generations.
timer = Timer 0 NilLS
[ TimerId timerID
, TimerSelectState Unable
, TimerFunction (noLS1 (\_->step))
]
// file defines the "File" menu, containing only the quit command to terminate the program.
file = Menu "&File"
( MenuItem "&About LifeGameExample..."
[MenuFunction (noLS (showAbout "Life" "LifeHelp"))]
:+: MenuSeparator []
:+: MenuItem "&Quit" [MenuShortKey 'q',MenuFunction (noLS closeProcess)]
) []
// options defines the "Options" menu to set the size of the displayed cells.
options = Menu "&Options"
( SubMenu "Cell Size"
( RadioMenu
[ (title (2^i),Nothing,Just (char i),noLS (newsize (2^i)))
\\ i<-[0..4]
] 4 []
) []
) []
where
title size = toString size +++ " * " +++ toString size
char i = toChar (fromChar '1'+i)
// commands defines the "Commands" menu to run and halt the computations of life generations.
commands= Menu "&Commands"
( MenuItem "&Erase Cells" [MenuId eraseID,MenuShortKey 'e',MenuFunction (noLS erase)]
:+: MenuItem "&Random Cells (FSK)" [MenuId randomID, MenuShortKey 'r', MenuFunction (noLS randomC)]
:+: MenuItem "&Play" [MenuId playID, MenuShortKey 'p',MenuFunction (noLS play)]
:+: MenuItem "&Halt" [MenuId haltID, MenuShortKey 'h',MenuFunction (noLS halt), MenuSelectState Unable]
:+: MenuItem "&Step" [MenuId stepID, MenuShortKey 's',MenuFunction (noLS step)]
) []
// play starts the computation of successive generations given the current set of life cells.
play :: (PSt Life) -> PSt Life
play life
= appListPIO
[ disableWindowMouse windowID
, disableMenuElements [eraseID,playID,stepID]
, enableMenuElements [haltID]
, enableTimer timerID
] life
// halt stops the computation of successive generations, but does not change the current generation.
halt :: (PSt Life) -> PSt Life
halt life
= appListPIO
[ enableWindowMouse windowID
, disableMenuElements [haltID]
, enableMenuElements [eraseID,playID,stepID]
, disableTimer timerID
] life
// step calculates the next generation and displays it.
step :: (PSt Life) -> PSt Life
step life=:{ls=state=:{gen,size},io}
# state = {state & gen=next}
# io = appWindowPicture windowID render io
# io = setWindowLook windowID False (True,look state) io
= {life & ls=state, io=io}
where
(next,died) = lifeGame gen
render = drawCells (drawCell size) next o (drawCells (eraseCell size) died)
// erase sets the current generation to empty and clears the window.
erase :: (PSt Life) -> PSt Life
erase life=:{ls=state,io}
# state = {state & gen=makeGeneration}
# io = setWindowLook windowID True (True,look state) io
= {life & ls=state, io=io}
randomCell :: ClickPoint Life Int Int -> Life
randomCell origin state=:{gen,size,seed} xbound ybound
# (x,seed) = random seed
# (y,seed) = random seed
# cell = makeLifeCell {origin & x=origin.x + (x rem xbound), y=origin.y + (y rem ybound)} size
# state=:{gen} = {state & gen=insertCell cell gen, seed=seed}
= state
randomCells :: ClickPoint Life Int -> Life
randomCells origin state count
| count <= 0 = state
| otherwise = randomCells origin (randomCell origin state 500 500) (count - 1)
// randomC sets the current generation to empty, clears the window, and then initializes the window with some "random" state.
randomC :: (PSt Life) -> PSt Life
randomC life=:{ls=state=:{gen,size},io}
// # state=:{seed} = {state & gen=makeGeneration}
# state=:{seed} = state
# io = setWindowLook windowID True (True,look state) io
# (viewframe, io) = getWindowViewFrame windowID io
origin = viewframe.corner1
# state=:{gen} = randomCells origin state 1000
# io = setWindowLook windowID True (True,look state) io
= {life & ls=state, io=io}
// newsize changes the size in which life cells are rendered and redraws the window.
newsize :: Int (PSt Life) -> PSt Life
newsize newSize life=:{ls=state=:{size=oldSize},io}
# state = {state & size=newSize}
# (viewframe,io)= getWindowViewFrame windowID io
oldOrigin = viewframe.corner1
newOrigin = {x=oldOrigin.x/oldSize*newSize,y=oldOrigin.y/oldSize*newSize}
# io = setWindowLook windowID False (True,look {state & gen=makeGeneration}) io
# io = setWindowViewDomain windowID (getViewDomain newSize) io
# io = moveWindowViewFrame windowID (toVector newOrigin-toVector oldOrigin) io
# io = setWindowLook windowID True (True,look state) io
= {life & ls=state, io=io}
// The window look:
look :: Life SelectState UpdateState *Picture -> *Picture
look {gen,size} _ {newFrame} picture
# picture = unfill newFrame picture
# picture = drawCells (drawCell size) gen picture
= picture
// The window mouse accepts only MouseDown user actions:
onlyMouseDown :: MouseState -> Bool
onlyMouseDown (MouseDown _ _ _) = True
onlyMouseDown (MouseDrag _ _) = True
onlyMouseDown _ = False
// The window mouse action places and removes alive cells:
track :: MouseState (PSt Life) -> PSt Life
track mouse life=:{ls=state=:{gen,size},io}
| modifiers.commandDown
# state = {state & gen=removeCell cell gen}
# io = appWindowPicture windowID (eraseCell size cell) io
# io = setWindowLook windowID False (True,look state) io
= {life & ls=state, io=io}
| otherwise
# state = {state & gen=insertCell cell gen}
# io = appWindowPicture windowID (drawCell size cell) io
# io = setWindowLook windowID False (True,look state) io
= {life & ls=state, io=io}
where
(pos,modifiers) = case mouse of
(MouseDown pos mods _) -> (pos,mods)
(MouseDrag pos mods) -> (pos,mods)
cell = makeLifeCell pos size
// Given the size in which to render life cells, getViewDomain calculates the corresponding ViewDomain:
getViewDomain :: CellSize -> ViewDomain
getViewDomain size
= {corner1={x=size*left,y=size*top},corner2={x=size*right,y=size*bottom}}
where
{corner1={x=left,y=top},corner2={x=right,y=bottom}} = Universe
// Program constants.
Universe :== {corner1={x=(-1000),y=(-1000)},corner2={x=1000,y=1000}}
StartCellSize :== 8