#732 Example code I'm donating, along with a puzzle

yachris Sat 29 Aug 2009

Hello,

I wrote the following to get clear on a couple of concepts in fan (Files, Maps):

// Example of files and maps

class Wordcount
{
  Void main(Str[] args)
  {
    if (args.size != 1)
    {
      echo("Usage: Wordcount file_to_read")
      Sys.exit(-1)
    }

    // Set up our map to count each word, and set its default to zero
    word_counts := [Str:Int][:]
    word_counts.def = 0

    try
    {
      // Open the file, read each line in order
      in_file := Uri(args[0]).toFile
      in_file.eachLine |Str str|
      {
        // split on whitespace into words
        words := str.split

        // count each one
        words.each |word|
        {
          word_counts[word] += 1
        }
      }

      // Show each word found, with its count, in alphabetical order
      keys := word_counts.keys
      keys.sort.each |key|
      {
        echo("$key ${word_counts[key]}")
      }
    }
    catch (IOErr ioe)
    {
      echo("Caught $ioe")
    }
  }
}

I'd like to donate it as an example for the cookbook. Please let me know if anything is "non-standard" fan -- the only thing I'm doing which I don't usually do is make single-use variables (such as words and keys above), which I'm doing for explanation in the code above.

My puzzle is that, if I run it on some random text file I have lying around, it works fine. However, if I run it on its own source code, I wind up with five zero-length (!!!) strings being found. I found this out by changing the final echo to

echo("$key ${key.size()} ${word_counts[key]}")

I'm running on a mac with java version "1.5.0_19".

I'd be curious to know if it's happening for anyone else... I've run od -c on the file, and there are no special characters in the file.

tompalmer Sat 29 Aug 2009

It comes from your five empty lines. This behavior is also specified in the Str#split docs. (Though I looked it up only after investigating the program first.)

brian Sat 29 Aug 2009

very cool

I added into a new examples\sys directory - changeset

As Tom mentioned, the specification of Str.split with an empty string is to return a list of one element. Fix is to just skip empty lines.

Regarding conventions, I tweaked a couple of things:

  • typically I wouldn't catch an error in a simple example program since it adds noise from what we are trying to show
  • Fan uses camel case convention rather than underbars
  • I prefer to put things like setting the def value of a map in an it-block as part of the construction process

But other than that, looks like pretty nice Fan code.

Thanks much!

yachris Sat 29 Aug 2009

Hi Brian,

Thanks for the explanation, and the kind words. I put in the try/catch since when it's run on a non-existent file, you get 24 lines (!!!) of backtrace. Not exactly user-friendly :-/ as opposed to the one-line output of the catch output.

However, one thing I kind of don't like:

// skip empty lines
if (line.trim.isEmpty) return

In the context of an eachLine I guess it makes sense to use a return, but my feeling is that it really should be a continue, since it's conceptually a loop; the return looks like it's returning from the main.

brian Sat 29 Aug 2009

In the context of an eachLine I guess it makes sense to use a return, but my feeling is that it really should be a continue, since it's conceptually a loop;

That is actually a "tricky" aspect of Fan - closures are true nested functions. This means that return actually returns from the closure (not the containing method).

So whenever you see a closure open |...| it means that you are in a nested function.

To avoid real confusion, we don't allow the return keyword to be used inside an it-block (which is itself just a closure).

yachris Sat 29 Aug 2009

Okay, I understand what's happening from the compiler's point of view.

It just is kind of jarring... violates the Principle of Least Surprise(1). Of course, I'm coming from rubylandia, where next (Ruby's equivalent to continue) does The Right Thing(TM) inside eachLine type loops.

And, I think it's worth considering that inside (say) an eachLine, it would be nice to return from the method we're currently in, and return is a nice way to do that.

~~~~~~~~~~~~~~~

(1) The Principle of Least Surprise is just an encoding of my prejudices, set in formal language to compel your instant obedience :-)

brian Sat 29 Aug 2009

I can definitely understand that, but it depends on your perspective. If you think of a closure a loop, then it is inconsistent. But if you think of a closure as a nested function then it is consistent. Fan actually treats functions in a much more consistent way than Ruby blocks.

We have had several discussions about a non-local return from within a closure. The general consensus (and many disagree) is that it is too complicated because under the covers it requires throwing an exception. Typically where you would do a continue you can use a return, and where you would do a break you use a specialized iterator such as sys::List.find or sys::List.eachWhile.

Login or Signup to reply.