CleanLifeDevDiary

Felix describes his experiences trying to work with Clean on Mac OS X.

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.

Thursday, June 02, 2005

 

a final refactoring + feature

Now that I've managed to actually generate multiple random cells, its time to make them actually produce interesting output!


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}

 

Finally, randomness (=> happiness)

Okay, my last (and most devious) problem was one of threading state and rebinding the variables to point to that state.

Can you see the bug in the following code?

 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}


Hint: it manifests itself as only a single cell being randomly generated in response to the randomC command.

 

ARGH!

My type inference problem wasn't a type inference problem at all!

It was an issue where I was defining a function named "random" which is a *PSt Life -> *PSt Life, and I was also importing the standard library function "random" which produces a tuple (Int, RandomSeed).

Amazing. Why didn't the module interface checking pick up on the name collision, and warn me about it? Why was this delayed to type checking time?

 

"big" risks

Okay, having read the implementation of the random function, I decided that I was just going to have to grab whatever (possibly large) value it hands to me, and take that value modulo some small value to get a small value in a small range.

So, i went ahead and hacked up the following lines of code for 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

 

how to change your life

In the current implementation, the only way to change the initial blank board is to click on cells with your mouse, which toggles the state of that cell (they all start off with the default of dead, and toggling makes dead cells alive).

Here is the code for the mouse handler:

// 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

 

making "random" random.

Okay, my last post gave my intitial delta, and the comments outlined the changes that were necessary.

For completeness, here is the definition of 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}

So, the question now is: how do I actually initialize the state so that I get a random distribution over some area of the display, rather than the currently implementation, which satisifies the specification only in a particularly narrow sense...

Time to review the Clean std library to see if there is a random function there.

 

First change! ("Random" menu item)

Okay, I made my first delta to the LifeGameExample source code.

It was a pretty easy change to isolate, and I just followed the pattern implied by the surrounding expression, to end up with the following new line of code:
:+: MenuItem "&Random Cells (FSK)" [MenuId randomID, MenuShortKey 'r', MenuFunction (noLS random)]


Of course, I don't know what half of the stuff up there does. What's :+:? What's noLS? Search me. I'm going to just click "go" and see what the type checker tells me.

Here is the full source for the "final" product, as of 4:30 EST on June 2nd.
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

Archives

June 2005  

This page is powered by Blogger. Isn't yours?