Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with:

Seven Languages In Seven Weeks: Io - Day 2

By Ben Nadel on
Tags: Io

I just finished day 2 of my Io language homework from Seven Languages in Seven Weeks. After the relative ease of day 1, this set of homework problems was downright intense! I would estimate that the following problem set took me a good 4-5 hours to complete (and then another hour or so just to write up). And, I couldn't even solve the last one entirely - you'll see that I cannot get rid of a "nil" from my standard output.

Don't get my wrong though - the homework was a lot of fun! Just exhausting. And again, I think we are only going to get out of the homework what we put into it, so I am trying to make it a point to really dig into the language and see how it works. That said, let's get to it.

HW1: A Fibonacci sequence starts with two 1s. Each subsequent number is the sum of the two numbers that came before: 1, 1, 2, 3, 5, 8, 13, 21, and so on. Write a program to find the nth Fibonacci number: fib(1) is 1, and fib(4) is 3. As a bonus, solve the problem with recursion and with loops.

  • // A Fibonacci sequence starts with two 1s. Each subsequent number
  • // is the sume of the two numbers that came before: 1, 1, 2, 3, 5,
  • // 8, 13, 21, and so on. Write a program to find the nth Fibonacci
  • // number: fib(1) is 1, and fib(4) is 3. As a bonus, solve the
  • // problem with recursion and with loops.
  •  
  • // Define the FIB method in the global (Lobby) name space.
  • fib := method( nth,
  • // First, check to see if we are dealing with base values; that
  • // is, values 1th and 2th that are not base on previous sums.
  • if(
  • ((nth == 1) or (nth == 2)),
  • return( 1 );
  • );
  •  
  • // If we have made it this far, then we are dealing with values
  • // that we need to calculate. We shall loop up to the nth value,
  • // building an aggregate sum.
  •  
  • // Start out with the known base values.
  • nthSub2 := 1;
  • nthSub1 := 1;
  •  
  • // As we iterate up to our target nth, we're going to need an
  • // intermediary value to keep track of the current sum before
  • // we shift the "view" to (nth-1) and (nth-2).
  • nthSub0 := (nthSub1 + nthSub2);
  •  
  • // Loop up to the nth position within the sequence. Since we
  • // already know the values at position 1 and 2, we are going
  • // to start this iteration at 3.
  • for( i, 3, nth, 1,
  •  
  • // Get the sum at this position.
  • nthSub0 = (nthSub1 + nthSub2);
  •  
  • // Shift (nth-1) and (nth-2) values down for the next
  • // iteration.
  • //
  • // NOTE: These values are now only relevant on the (ith+1)
  • // loop. They are no longer meaningful for the ith loop.
  • nthSub2 = nthSub1;
  • nthSub1 = nthSub0;
  •  
  • );
  •  
  • // Return the last computed sum in the sequence.
  • return( nthSub0 );
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Test the first 10 values of the Fibonacci sequence.
  • fib( 1 ) println();
  • fib( 2 ) println();
  • fib( 3 ) println();
  • fib( 4 ) println();
  • fib( 5 ) println();
  • fib( 6 ) println();
  • fib( 7 ) println();
  • fib( 8 ) println();
  • fib( 9 ) println();
  • fib( 10 ) println();

This approach uses a simple for() loop to iterate from 3 to nth, calculating the sum as we go. Since the first two positions are always "1", we only have to perform a calculation if the desired position is 3 or greater.

Running the above code, we get the following console output:

1
1
2
3
5
8
13
21
34
55

Now that we've solved it with a straightforward for() loop, let's revisit the problem with recursion. At first, I wanted to make the top-level fib() function recursive; however, I felt that would have required a dynamic argument list, which seemed like unnecessary overhead. As such, I defined a secondary method, fibRecursive(), local to the fib() method that will handle the recursion for us:

  • // A Fibonacci sequence starts with two 1s. Each subsequent number
  • // is the sume of the two numbers that came before: 1, 1, 2, 3, 5,
  • // 8, 13, 21, and so on. Write a program to find the nth Fibonacci
  • // number: fib(1) is 1, and fib(4) is 3. As a bonus, solve the
  • // problem with recursion and with loops.
  •  
  • // Define the FIB method in the global (Lobby) name space. This time,
  • // we are going to solve the problem recursively. However, in order
  • // to make the method signature simple, our recursive function shall
  • // be defined within the local function context.
  • fib := method( nth,
  • // First, we want to check to see if we even need to solve
  • // anything. If the nth values is 1 or 2, we can hard-wire the
  • // results for those positions.
  • if(
  • ((nth == 1) or (nth == 2)),
  • return( 1 );
  • );
  •  
  • // If we have made it this far, then we are dealing with values
  • // that we need to calculate. We shall now recursively find the
  • // value at the given position within the sequence.
  •  
  • // Define the local method that actuall performs the recursion.
  • // In this case, nth is our target position; ith is our current
  • // position within the sequence.
  • fibRecursive := method( nth, ith, nthSub2, nthSub1,
  •  
  • // Check to see if we have reached the target position within
  • // the Fibonacci sequence. If so, then simply return the sum
  • // of the last two values.
  • if(
  • (nth == ith),
  • return( nthSub2 + nthSub1 );
  • );
  •  
  • // If we have not reached our target position, we are going
  • // to call this method recursively, digging one position
  • // deeper into the Fibonacci sequence.
  • return(
  • fibRecursive(
  • nth,
  • (ith + 1),
  • nthSub1,
  • (nthSub2 + nthSub1)
  • )
  • );
  •  
  • );
  •  
  • // Return the value at the given position. Since we know the
  • // first two values in the sequence (1 and 1), we are going
  • // to start looking at position 3.
  • return(
  • fibRecursive( nth, 3, 1, 1 )
  • );
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Test the first 10 values of the Fibonacci sequence.
  • fib( 1 ) println();
  • fib( 2 ) println();
  • fib( 3 ) println();
  • fib( 4 ) println();
  • fib( 5 ) println();
  • fib( 6 ) println();
  • fib( 7 ) println();
  • fib( 8 ) println();
  • fib( 9 ) println();
  • fib( 10 ) println();

The fibRecursive() function works by taking the target position, the current position, and the last two values (nth-2 and nth-1). Once the current position and the target position are the same, it simply returns the sum of the last two value; until then, however, it returns the result of the fibRecursive() function which is called on the next position (ith+1).

I chose to run my recursion in an upward direction. I suppose, however, that you could have run the recursion in the reverse direction - nth down to 1. I'm not sure if one direction has more merit than the other. But, since each value required two previously calculated values, recursing up felt more straightforward than recursing down.

Running the above code, we get the following console output:

1
1
2
3
5
8
13
21
34
55

The recursive approach works. However, in this kind of a situation, I would assume that the overhead of the growing call stack is probably not worth it. I love the concept of recursion, but I would imagine that the for() loop would be much more performant.

HW2: How would you change / to return 0 if the denominator is zero?

  • // How would you change / to return 0 of the denominator is zero?
  •  
  • // We are need to override the core division functionality; however,
  • // since the original is necessary for our ultimate purpose, we don't
  • // want to lose a reference to it. As such, we are going to copy the
  • // original division "message" into a new slot of the Number object.
  • //
  • // NOTE: Since all operators are just methods on objects, we both get
  • // and set operator methods using getSlot() and setSlot().
  • Number setSlot(
  • "coreDivision",
  • Number getSlot( "/" )
  • );
  •  
  • // Now that we have our original operator safely referenced, we can
  • // override the "/" operator on the Number object with our new logic.
  • Number setSlot(
  • "/",
  • method( denominator,
  • // Check to see if the denominator is zero. If so, we want
  • // to return zero, bypassing any error.
  • if (
  • (denominator == 0),
  • return( 0 );
  • );
  •  
  • // If the denominator is not zero, we want to pass the
  • // control off to the original division operator for the
  • // actual math to take palce.
  • return(
  • self coreDivision( denominator )
  • );
  • )
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Test the new operator by trying to divide by zero.
  • (4 / -2) println();
  • (4 / -1) println();
  • (4 / 0) println();
  • (4 / 1) println();
  • (4 / 2) println();

Here, we need to override the core division operator. And, since pretty much every operator in the Io language is actually a method on an object, overriding an operator simply requires overriding an existing method (slot) on the given object (Number in our case). Of course, at some point, we actually do want to perform the low-level math; as such, we need to keep a reference to the original division method. That's why I copy the "/" reference to a new slot - coreDivision - on the Number object before I overwrite it. That way, our proxy method will have a way to invoke the true division operation when the conditions are right.

Running the above code, we get the following console output:

-2
-4
0
4
2

As you can see, when we try to divide by zero, we get "0" rather than "inf" (infinity).

HW3: Write a program to add up all the numbers in a two-dimensional array.

  • // Write a program to add up all of the numbers in a two-dimensional
  • // array.
  •  
  •  
  • // Define the method as a function of the List class (the closest
  • // thing that Io has to an array). There is already a sum() method,
  • // so we'll create a deepSum() method, which will dive into any
  • // nested list instance.
  • List deepSum := method(
  • // Keep a running sum of all the values in this list. Since
  • // we don't know if we are going to have any values just yet,
  • // we are going to start off with nil.
  • runningSum := nil;
  •  
  • // Flatten the list to get rid of any nesting.
  • flattenedList := self flatten();
  •  
  • // Now, iterate over the flattened list, summing up any numeric
  • // values.
  • flattenedList foreach( index, value,
  •  
  • // Check to see if the value at the given position is of
  • // a numeric type. Since a list can contain any kind of data,
  • // we're only going to concern ourselves with the numeric
  • // values.
  • if (
  • (value type() == "Number"),
  •  
  • // Add this value to the running sum. If we have not,
  • // set a running sum yet, just return zero as the
  • // running value.
  • runningSum = (
  • value +
  • if( runningSum, runningSum, 0 )
  • );
  • );
  • );
  •  
  • // Return the running sum.
  • //
  • // NOTE: If we have not encountered any numeric values, this
  • // value might still be nil.
  • return( runningSum );
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Build a nested list.
  • nestedList := list(
  • list( 1, 2, 3 ),
  • list( 10, 20, 30 ),
  • list( 100, 200, 300 )
  • );
  •  
  •  
  • // Get the deep sum of the list.
  • nestedList deepSum() println();

The trick here is to take the nested lists (our two-dimensional array) and just flatten it. That gives us a single list to work with. At this point, we could have executed the sum() method on the list; however, sum() will throw an error if it encounters any non-numeric values. For this exercise, I wanted to simply ignore non-numeric values and only work with valid numbers. As such, I needed to manually iterate over the list using a foreach() loop.

Running the above code, we get the following console output:

666

HW4: Add a slot called myAverage to a list that computes the average of all the numbers in a list. What happens if there are no numbers in a list? (Bonus: Raise an Io exception if any item in the list is not a number).

  • // Add a slot called myAverage to a list that computes the average
  • // of all the numbers in a list. What happens if there are no
  • // numbers in a list? (Bonus: Raise an Io exception if any item in
  • // the list is not a number).
  •  
  •  
  • // Define the myArray method on the core List object.
  • //
  • // NOTE: We could have just used sum()/size() to accomplish the same
  • // thing, including the Io exception which will get raised when any
  • // non-numeric value is included in a sum() message. However, for
  • // practice sake, I will perform this a little bit more manually.
  • List myAverage := method(
  • // Check to see if we have any values in our list. If not,
  • // return nil.
  • if(
  • (self size() == 0),
  • return( nil );
  • );
  •  
  • // At this point, we know we have at least one value to average.
  • // As such, let's prepare our running total.
  • runningSum := 0;
  •  
  • // Loop over each value in the list.
  • self foreach( index, value,
  •  
  • // Check to see if the current value is a number. If not,
  • // we are going to raise an exception.
  • if(
  • (value type() != "Number"),
  •  
  • // Raise an exception. As part of our error message, we
  • // are reflectively adding the method name (so we don't
  • // have to hardcode it in the error).
  • Exception raise(
  • "NonNumericValue",
  • ("A non-numeric value [" .. value .. "] was encountered during the " .. (call message() name()) .. " operation.")
  • );
  • );
  •  
  • // If no exception was raised, we can add this numeric value
  • // to our running sum.
  • runningSum = (runningSum + value);
  •  
  • );
  •  
  • // Return the average of the sum over the size of the array.
  • return( runningSum / (self size()) );
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Try it once with no values.
  • list() myAverage() println();
  •  
  • // Try it once with only valid numbers.
  • list( 1, 2, 3, 4, 5 ) myAverage() println();
  •  
  • // Try it once with invalid values.
  • list( 1, 2, "Dang!", 4, 5 ) myAverage() println();

We could have easily accomplished the same thing using the sum() and size() methods of the list. Since sum() already raises an Io exception when it encounters a non-numeric value, that native behavior matches our desired behavior; however, since I wanted to learn about Io language exceptions, I wanted to, once again, resort to iterating over the list manually.

When I do raise an exception for non-numeric values, you'll notice that I am providing the name of the method - myAverage - within the error message; however, you won't see any string that actually says "myAverage." That's because I am using reflection and the call object to get the name of the current method as it was defined at runtime:

  • call message() name()

When we run the above code, we get the following console output:

nil
3
Exception: NonNumericValue
Nested Exception: 'A non-numeric value [Dang!] was encountered during the myAverage operation.'

You'll notice that the first average of an empty list returned nil. Then, the second average of five numbers returned 3. Our third example, which contained an invalid string - Dang! - resulted in our Io language exception.

HW5: Write a prototype for a two-dimensional list. The dim(x,y) method should allocate a list of y lists that are x elements long. set(x,y,value) should set a value. get(x,y) should return that value.

  • // Write a prototype for a two-dimensional list. The dim(x,y)
  • // method should allocate a list of y lists that are x elements
  • // long. set(x,y,value) should set a value. get(x,y) should return
  • // that value.
  •  
  •  
  • // Clone List in order create a prototype for our multi-dimentional
  • // list. We are using the Upper case name to denote type.
  • MultiList := List clone;
  •  
  •  
  • // I allocate enough space for a two-dimentional list.
  • //
  • // NOTE: This is a "static" method. By that, I mean that it must
  • // work on a cloned instance of the MultiList object. To ensure that
  • // this is true, we check the instance prototype before proceeding.
  • MultiList dim := method( x, y,
  •  
  • // Check to make sure that the DIM method is not being called
  • // on the core MultiList instance. We want the class to be
  • // cloned before it is altered. We will know this based on the
  • // current instance prototype. If it is still List, then we
  • // haven't cloned the MultiList object.
  • if(
  • (self proto() type() == "List"),
  •  
  • // Clone the class and return the new instance.
  • return(
  • MultiList clone() dim( x, y )
  • );
  • );
  •  
  •  
  • // First, allocate enough items in this list to hold our
  • // nested lists.
  • self setSize( x );
  •  
  • // Now, let's loop over each index and create a nested list
  • // that is preallocated to hold a y items.
  • for( i, 0, (x - 1), 1,
  •  
  • // Create, allocate, and set the nested list.
  • self atPut(
  • i,
  • (list() setSize( y ))
  • );
  •  
  • );
  •  
  • // Return self for method chaining.
  • return( self );
  • );
  •  
  •  
  • // I get the value at the given x,y position.
  • MultiList get := method( x, y,
  • return(
  • self at( x ) at( y )
  • );
  • );
  •  
  •  
  • // I set the value at the given x,y position.
  • MultiList set := method( x, y, value,
  • // Get the top-level list and then set the nested value.
  • self at( x ) atPut( y, value );
  •  
  • // Return self for method chaining.
  • return( self );
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  •  
  • // Create a multi-dimensional list. Since the dim() method is
  • // a "static" method, we can allow it to create a new instance
  • // of MultiList for us.
  • matrix := MultiList dim( 3, 3 );
  •  
  • // Populate the matrix.
  • matrix set( 0, 0, "A" );
  • matrix set( 1, 1, "B" );
  • matrix set( 2, 2, "C" );
  •  
  • // Print the list to see how sets worked.
  • matrix println();
  •  
  • // Get the values:
  • ("0x0 : " .. matrix get( 0, 0 )) println();
  • ("1x1 : " .. matrix get( 1, 1 )) println();
  • ("2x2 : " .. matrix get( 2, 2 )) println();

Since our MultiList object is essentially a specialist List, I started off with a clone of the core List object. This way, our list of lists could leverage all of the base List functionality. The part where I tried to get tricky in this problem was the dim() method. I wanted to make sure that the dim() method was only ever executed on a cloned instance of the MultiList object. To do this, I check the prototype object (proto) of the instance on which dim() is being executed. If the prototype was List, I knew that I was working with the MultiList object directly (since it was a clone of List and the cloned object becomes the resultant object's prototype). If the prototype was not a list (ie. the prototype was MultiList), then I knew I was working with a clone and could proceed.

When we run the above code, we get the following console output:

list(list(A, nil, nil), list(nil, B, nil), list(nil, nil, C))
0x0 : A
1x1 : B
2x2 : C

HW6: Bonus: Write a transpose method so that new_matrix(y,x) == matrix(x,y) on the original list.

I had to read this problem like 5 times before I even understood what it was asking. I finally noticed that the x/y coordinates were flipped in the new_matrix. I guess transposing is some sort of reflection (it's been a looooong time since I took Finite Math with Mrs. Kaplan and had to deal with mutating matrices).

  • // Bonus (on HW5): Write a transpose method so that (new_matrix
  • // get(y,x) == matrix(x,y) on the original list.
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  • // Import the hw5 answer. This will give us access to the MultiList
  • // class which we can then augment in this homework.
  • //
  • // NOTE: This will output the results of the HW5... which we will
  • // ignore.
  • doFile( "hw5.io" );
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Augment the MultiList class to have a transpose() method which
  • // flips the matrix diagnoly.
  • MultiList transpose := method(
  • // Get the dimensions of the current matrix.
  • //
  • // NOTE: We are assuming a valid matrix coming into this. IE,
  • // we are relying on a the second dimension being >= 1.
  • xDimension := self size();
  • yDimension := self at( 0 ) size();
  •  
  • // Create a new matrix with the same dimensions. When doing,
  • // we are going to reverse the dimensions.
  • matrix := MultiList clone() dim(
  • yDimension,
  • xDimension
  • );
  •  
  • // Loop over every item in the current matrix and transpose
  • // it into the new matrix.
  • for( x, 0, (xDimension - 1),
  • for( y, 0, (yDimension - 1),
  •  
  • // Copy to reflected position in new matrix.
  • matrix set(
  • y,
  • x,
  • self get( x, y )
  • );
  •  
  • );
  • );
  •  
  • // Return transposed matrix.
  • return( matrix );
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Update the matrix from HW5 (which we have from the doFile()
  • // method call).
  • matrix set( 0, 2, "TR" );
  • matrix set( 2, 0, "BL" );
  •  
  • // Print the current matrix.
  • matrix println();
  •  
  • // Now, transpose the matrix.
  • reflectedMatrix := matrix transpose();
  •  
  • // Print the new matrix.
  • reflectedMatrix println();
  •  
  • // And, assert the equality in the two corners.
  • (matrix get( 0, 2 ) == reflectedMatrix get( 2, 0 )) println();
  • (matrix get( 2, 0 ) == reflectedMatrix get( 0, 2 )) println();
  •  
  • // Assert that the same coordinates are not equal.
  • (matrix get( 2, 0 ) != reflectedMatrix get( 2, 0 )) println();

The first thing I do here is call doFile() on my previous homework problem, "hw5.io". This includes the given file and executes it much like ColdFusion's CFInclude. Doing this gives our current execution context access to the MultiList object from the previous problem. And, once I have the MultiList object and the matrix variable (also from HW5), I can go about adding the transpose() method.

Within the transpose() method, I use a junky approach to getting the original x and y dimensions - I test for them on the current object. What I would rather have done is store the x and y dimensions as object properties when the dim() method was called; however, too little too late.

Once I have the dimensions of the current list, I simply use a nested for() loop to iterate over the current matrix, transposing values onto a new matrix of reflected dimensions. When we run the above code, we get the following console output:

list(list(A, nil, TR), list(nil, B, nil), list(BL, nil, C))
list(list(A, nil, BL), list(nil, B, nil), list(TR, nil, C))
true
true
true

NOTE: The output from HW5 was not included in the above result, although it was part of the output generated by doFile( "hw5.io" );

As you can see the TR (Top-Right) and BL (Bottom-Left) matrix values have been transposed. Furthermore, all of the equality assertions are true.

HW7: Write the matrix to a file, and read a matrix from a file.

Rather than deal with including the MultiList object again, I simply hardcoded a matrix for this problem. Since this was more about File Input/Output, I felt the previous problems were not relevant.

  • // Write the matrix to a file, and read a matrix from a file.
  •  
  •  
  • // For this problem, I'm not going to use the same matrix from the
  • // previous problems as it's just a pain to include the
  • // functionality. Rather, I'm just going to manually create a
  • // matrix to write and read.
  •  
  • matrix := list(
  • list( "A", 0, 0 ),
  • list( "0", "B", "0" ),
  • list( "0", "0", "C" )
  • );
  •  
  •  
  • // I am the file path of the file we will be creating.
  • filePath := "./hw7.txt";
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Create / open the file for writing.
  • dataFile := File open( filePath );
  •  
  • // Write the matrix to the file. Since write() expects a string,
  • // we are going to pass it a serialized form of the list. This
  • // will give us something that looks like an actual Io code
  • // representation of the list construct.
  • dataFile write( matrix serialized() );
  •  
  • // Close the file that we have just written.
  • dataFile close();
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Now that are file contains the serialized version of the matrix,
  • // we can read that file back in and execute it using the doFile()
  • // method. doFile() executes the given file in the context of the
  • // receiver (Lobby in our case). The result of the execution (our
  • // list) is returned, and stored in the doMartrix value.
  • doMatrix := doFile( filePath );
  •  
  • // Print out the two matrix values to make sure they are equal.
  • // I am outputting the size of the two matrix just to demonstrate
  • // that they are actually List implementations and not just strings
  • // that *look* like lists.
  • ("Matrix (size: " .. matrix size() .. ")") println();
  • matrix println();
  •  
  • ("doFile Matrix (size: " .. doMatrix size() .. ")") println();
  • doMatrix println();

In this case, we are serializing our matrix using the serialized() method. This is like ColdFusion's serializeJSON(), but it serializes the object into an Io message string. This Io message string can then be read in and executed using the doFile() method. Notice that the result of the doFile() execution is being assigned to a new variable.

When we run the above code, we get the following console output:

Matrix (size: 3)
list(list(A, 0, 0), list(0, B, 0), list(0, 0, C))
doFile Matrix (size: 3)
list(list(A, 0, 0), list(0, B, 0), list(0, 0, C))

As you can see, doFile() not only read in the data file, it resulted in a valid List instance. The binding rules behind doFile() seem to be a bit more complex than I needed to know for this homework; but, from what I read, it looks like a doFile() method, when executed on an object:

  • myObject doFile( "filePath" )

... will actually execute the included file within the context of that object. I assume this is akin to running a CFInclude tag within a CFComponent in ColdFusion.

HW8: Write a program that gives you ten tries to guess a random number from 1-100. If you would like, give a hint of "hotter" or "colder" after the first guess.

I couldn't solve this problem completely. I got something working; however, as you'll see, my output is getting muddied with a "nil" value whenever the standard input is read. I have no idea why that is happening and I don't know what to do about it.

  • // Write a program that gives you ten tries to guess a random number
  • // from 1-100. If you would like, give a hint of "hotter" or
  • // "colder" after the first guess.
  •  
  •  
  • // The first thing we need to do is select a random number. Random()
  • // allows us to create a floating point number between 0 and N. As
  • // such, to go from 1-100, we are going to go to 99 and add 1.
  • randomNumber := (Random value( 99 ) + 1) floor();
  •  
  •  
  • // To gather input from the user, we are going to create a file input
  • // stream that uses the Standard Input (stdio) as the input.
  • standardIO := File standardInput();
  •  
  •  
  • // Because we are going to be giving the user "hotter" and "colder"
  • // feedback, we need to keep track of the previous guess.
  • previousGuess := nil;
  •  
  • // We are only going to give the user up to 10 guesses. We can create
  • // a short-hand for a 10x loop with the repeat() method.
  • 10 repeat(
  •  
  • // Prompt the user for the number.
  • "Guess number (1..100): " println();
  •  
  • // Read the value from the user and parse it as number.
  • guess := standardIO readLine() asNumber();
  •  
  • // Check to see if the guess is correct.
  • if(
  • (guess == randomNumber),
  •  
  • // Break out of the loop. The user is done guessing.
  • break;
  • );
  •  
  • // Check to see if we have a previous guess yet.
  • if(
  • previousGuess,
  •  
  • // Check to see if we are getting closer or farther away
  • // from the target number. We are just going to test to see
  • // if the distance between the guess and the target number
  • // is getting smaller or larger.
  • if(
  • ((randomNumber - guess) abs()) >= ((randomNumber - previousGuess) abs()),
  • "Getting colder :(" println(),
  • "Getting warmer :)" println()
  • );
  • );
  •  
  • // Store the previous guess so we can calculate our warmer /
  • // colder message on the next guess.
  • previousGuess = guess;
  • );
  •  
  •  
  • // Now that the user is done guess, we have to see if they guessed
  • // correctly (broke out of loop) or if they guessed poorly (loop
  • // ended naturally).
  • if(
  • (guess == randomNumber),
  • (
  • "Awesome! Excellent guess!" println();
  • ),
  • (
  • "Sorry, better luck next time." println();
  • )
  • );

I won't really talk about this one since I don't think I solved it properly. However, when I run the above code, and interact with it, I get the following console output:

Guess number (1..100):
nil50
Guess number (1..100):
nil25
Getting warmer :)
Guess number (1..100):
nil12
Getting colder :(
Guess number (1..100):
nil40
Getting colder :(
Guess number (1..100):
nil30
Getting warmer :)
Guess number (1..100):
nil27
Getting warmer :)
Guess number (1..100):
nil26
Getting warmer :)
Guess number (1..100):
nil24
Getting warmer :)
Guess number (1..100):
nil23
Awesome! Excellent guess!

Wow, that homework set was just exhausting. Still, I think I am getting a much better handle on the language. The massive amount of parenthesis are even starting to make sense to me, although I am sure die-hard Io language programmers would tell me the amount of "noise" I'm adding is absurd.

Tweet This Great article by @BenNadel - Seven Languages In Seven Weeks: Io - Day 2 Thanks my man — you rock the party that rocks the body!



Reader Comments

@Wil,

Booya! That's what I'm talking about. It's a lot of fun. As someone how has been primarily ColdFusion for last 8 years, it's a very nice shift in worldly understanding.

Reply to this Comment

Am I the only one who thinks that Ben's code in *other* languages is more pleasing to look at than his CF code? ;-)

Reply to this Comment

These articles on Io have been a great help as I've been going through the book myself. I think I just figured out the problem in HW8 where it outputs 'nil'.

That's the prompt. Try this:

guess := standardIO readLine("Guess number (1..100): ") asNumber();

Hope this helps.

Reply to this Comment

@Chris,

It looks like it has something to do with the combination of N repeat() and the I/O. I tried running this in the command line:

  • 1 repeat (
  • "Guess number (1..100): " println();
  • File standardInput() readLine() asNumber();
  • );

This outputs:

Guess number (1..100):
nil

If I do this outside of the repeat() function, it works, no "nil" output. Must be something specific to the repeat() method?

Reply to this Comment

@Ben,

Just running it in the shell, if I simply call:

File standardInput readLine

I get:

nil

Then it waits for me to enter something. Entering:

File standardInput readLine("Enter something: ")

however, prompts me with:

Enter something:

The reference guide on iolanguage.com appears to be down, so I can't verify that. Trying this:

3 repeat(guess := File standardInput readLine("Enter a number: ") asNumber; guess println)

Gives me:

Enter a number: 3
3
Enter a number: 20
20
Enter a number: 27
27

Trying it your way does give the same results you had, which leads me to believe the 'nil' is the default prompt. As for why it only happens inside a repeat block, I'm not quite sure.

Reply to this Comment

Chris is correct. If you give it a prompt string, you will not get the nil.

Why does this happen? Well, I was curious myself, and since I am going through the book as well, I figured I'd investigate. A little playing reveals this...

  • Io> File getSlot("readLine")
  • ==> File_readLine()
  • Io> File getSlot("readLine") type
  • ==> CFunction

which does not tell us much except that it is a C function. So, I went to see what that function did with the prompt. Since I installed from source, a quick grep revealed the C function, but it does not take a prompt. So, there must be something else going on. Maybe the standardInput method returns something slightly different from a File...

  • Io> File standardInput getSlot("readLine")
  • ==> # /home/jhagins/src/Io/stevedekorte-io-2505005/libs/iovm/io/Z_CLI.io:5
  • method(prompt,
  • prompt print
  • resend
  • )

Ahhh, it becomes a bit more clear. The object returned by

  • File standardInput

defines its own method in the readLine slot. The first thing that method does is print the prompt (even if it is nil).

We can fix it "temporarily" like so...

  • Io> File standardInput readLine = method(prompt, if (prompt, prompt print); resend)
  • ==> method(prompt,
  • if(prompt, prompt print); resend
  • )
  • Io> File standardInput readLine
  • Look, no "nil"
  • ==> Look, no "nil"
  • Io>

However, it does not fix it permanently. However, it looks like the code is in an Io source file. Maybe that file gets loaded when Io starts, and we can simply change it. Unfortunately, that is NOT what happens. Looking at the source code, those IO files are munged into a C file that is then compiled into the executable.

On the other hand, running strace on the Io interactive shell, I see that it loads several .io files, namely:

  • .iorc { expects to find this in the home directory }
  • main.io { looks in the current directory }

It also looks for some other stuff (like command line Readline/Editline support and a history of Io commands). So, I'll give that a try. Here is my ~/.iorc

  • // Fix a bug that emits "nil" if a prompt is not provided.
  • File standardInput readLine = method(prompt,
  • if (prompt, prompt print)
  • resend)

Now, let's crank up the Io command line interpreter.

  • shell prompt -> io
  • Io 20090105
  • Io> File standardInput getSlot("readLine")
  • ==> # /home/jhagins/.iorc:3
  • method(prompt,
  • if(prompt, prompt print)
  • resend
  • )
  • Io>

Hey, look! We now have a "fixed" version. However, you can always go back and fix the source code, and recompile and then you don't need this. However, this certainly shows a benefit. We are actually able to patch the language itself when it starts up. Just to make sure it works when NOT in the command line, create a sample script "xx.io"

  • File standardInput getSlot("readLine") println

Now, run it from the command line

  • shell-prompt -> io xx.io
  •  
  • # /home/jhagins/.iorc:3
  • method(prompt,
  • if(prompt, prompt print)
  • resend
  • )

Bingo! That's what we are looking for.

I am concerned, though, that I've "fixed it" in a bad way, because the bad code is in the implementation of DummyLine, and that may not always be what is returned by

  • File standardInput

.

So, it's better to change the fix in ~/.iorc to be:

  • // Fix a bug that emits "nil" if a prompt is not provided.
  • DummyLine readLine = method(prompt,
  • if (prompt, prompt print)
  • resend)

I hope this is clear, but I probably made it "clear as mud" since I'm discovering it as I type.

Reply to this Comment

It can't seem to find a way to edit the post. I left out a part. When I looked at the .io file, I saw this code, which is how I knew it was implemented:

  • DummyLine := File standardInput do(
  • readLine := method(prompt,
  • prompt print
  • resend
  • )
  • )

Later on, the DummyLine is used as the default way to read input from the keyboard. It also looks like it is replaced if you have the Readline/Editline addons enabled.

One more thing, regarding posting: It seems that I can not use the code html tag in the middle of a line. It always creates a new block. How do I differentiate code within prose?

Reply to this Comment

@Chris,

Ahhh, awesome find on the readLine( "prompt" )! Thank you kindly.

@Jody,

Wow - really fascinating exploration. You definitely took it way deeper than any understanding I've gotten out of the language. Some of the things you've mentioned I've never even heard of, like running a trace on a command file? I wish I knew more about how all this stuff was executing behind the scenes.

Thanks to both of you (Jody and Chris) for the help on understanding where my nil was coming from. Seeing the source code behind the methods is really an eye-opener.

As far as the CODE mid-line, when I build the commenting stuff that approach had never occurred to me. Someone else recently brought that to my attention as well. He suggested just checking to see if the CODE tag was followed by a line break. If it is, use a block-approach; otherwise, use an inline-approach. I'll get around to updating that at soon... hopefully.

Reply to this Comment

@Ben,

There are several unix commands to trace a process as it executes. You should have those tools available to you under MacOSX as well.

strace will trace all the system commands. You can run any program through it...

shell prompt -> strace ls -l

will run the command "ls -l " and send a trace of all the system calls to stderr.

Likewise, ltrace will trace library calls.

You can attach to an existing process as well (I think both take -p <process id> to indicate the process to trace).

Reply to this Comment

I think this is a better recurisve version of fib:

  • fib := method(n,
  • if(n == 0) then(return 0) elseif(n == 1) then(return 1) else(return fib(n-2) + fib(n-1))
  • )

Reply to this Comment

fib2 := method(nth,
if ((nth == 1) or (nth == 2),
return (1);
);
nth := nth -1;
return fib(nth-1) + fib(nth);
);

Reply to this Comment

This was an interesting approach. I preferred doing the fibonacci numbers using lists:

<pre>

  • fib := method(nth,
  • nth = nth - 1; numbers := list(1, 1)
  • if(nth < 0,
  • 0,
  • while(numbers size <= nth,
  • numbers append(list(numbers last, numbers at(-2)) sum)); numbers last)
  • )

</pre>

Reply to this Comment

Using the list method (as suggested by secondplanet), when you set numbers to (1,1) your sequence is off by one place. For example, fib(2) returns 2, not 1. Here's a suggestion:

  • fib := method (nth_place,
  • numbers := list(0,1)
  • while(numbers size <= nth_place,
  • numbers append(numbers at(-1) + numbers at(-2))
  • )
  • numbers at (nth_place)
  • )

Reply to this Comment

@Nate, @Secondplanet, @Johan,

Cool stuff. It's funny, I haven't looked at Io code in a few months and it already looks completely foreign to me :) I don't know how people can be fluent in multiple languages at any time. When I don't use it, it truly just leaves my mind.

Reply to this Comment

fib := method(n, f := method(n, a, b, if(n > 1) then(return f(n - 1, b, a + b)) else(return a)); return f(n, 1, 1))

Range 1 to(10) foreach(x, fib(x) println)

Reply to this Comment

Since everyone is showing off their code, let me join in: :-)

My Fibonacci code uses two methods, just because I don't like setting slots where I actually want local variables:

Number fib2 := method(a, b, t, (self - 2) repeat(t = b; b = b+a; a=t); b)
Number fib := method(self fib2(1,1,1))

Does anyone know a better method of getting local variables?

For number 3, I guess the simplest code is

List sum2D := method(self map(sum) sum)

The following myAverage will automatically throw an exception for non-numbers, I didn't quite understand where the "bonus" comes in (improving the error message to not refer to "+", probably, but sum does that, too …):

List myAverage := method(if(self size == 0, 0, (self sum)/(self size)))

The 2D list is a bit more involved:

Array := List clone
Array dim := method(i, j, empty; i = 1 to(i) asList map(0); j repeat(append(i clone)))

(note the "i clone" - obviously needed, but I forgot it at first.)

Array set := method(x, y, value, at(x) atPut(y, value); self)
Array get := method(x, y, at(x) at(y))

The transpose is tricky, but I found a way not needing locals - yay!

Array transpose := method(
if(size ==0, clone,
self clone copy(
at(0) map(i, v, self map(v, v at(i))))))

This uses the first row as a template for the list of rows to create (because it has the right length), replaces each entry of that list with the proper list, and then puts the resulting lists into a clone of self, to get the proper return type.

Reply to this Comment

Hi. I have just finished that question. Just wondering what you guys think of my solution:

  •  
  • standardIO := File standardInput()
  •  
  • random := Random value(1,100) floor
  • random println()
  •  
  • i := 1
  •  
  • for(i,1,10,i,
  • "Guess number (1..100): " println()
  • guess := standardIO readLine() asNumber()
  •  
  • if(random == guess, "You guessed it!" println() break)
  •  
  • if(guess>random, "Colder" println(), "Hotter" println() )
  • )

Reply to this Comment

hi Ben,
Thanks for the answer. it's pretty cool.
I bought this book in Chinese.

I don't know how to do some of the practices as questions made me feel confuse and the translation seems have a little mistake.
But after looked at your blog, I understood now.
thanks again, learn a lot from your code. :-D

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.