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

 

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?

Comments: Post a Comment

<< Home
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?