[ Content | View menu ]

Java.beyond

Mark Mzyk | September 14, 2008

Today’s title is a play off of Stuart Halloway’s current series of blog posts titled Java.next.  Stuart is focusing his series on the set of languages that run on the JVM and are looking to replace (or if not replace, then gain dominance alongside) Java.

In the third part of the series, Dispatch, Stuart writes about ways the Java.next languages dynamically choose behavior, one such way being with the switch statement.  Stuart provides examples in Ruby (technically he should have used JRuby), Groovy, Clojure, and Scala.  Stuart explores further than just the switch statement, but it was the switch statement example that caught my eye.

The example for each language is a simple piece of code that takes a grade (either numeric or letter) and returns the letter equivalent.  If passed a 95, the code returns A.  If passed 55, it returns F.  If passed B, it returns B.  If passed something that doesn’t equate to a valid grade, it throws an error.

I couldn’t resist coding it in Erlang.

Here is what I came up with:

-module(grades).
-export([grades/1]).

grades(N) ->
  case N of
    N when is_integer(N), N >= 90 -> "A";
    N when is_integer(N), N >= 80 -> "B";
    N when is_integer(N), N >= 70 -> "C";
    N when is_integer(N), N >= 60 -> "D";
    N when is_integer(N), N >= 0  -> "F";
    "A" -> "A";
    "a" -> "A";
    "B" -> "B";
    "b" -> "B";
    "C" -> "C";
    "c" -> "C";
    "D" -> "D";
    "d" -> "D";
    "F" -> "F";
    "f" -> "F";
    N when true -> throw("Not a valid grade")
 end.

Now for the embarrassing admission: this code took me way longer to complete than it should have.  I couldn’t remember the proper syntax for anything and I kept attempting to code Erlang like I was coding Python.  I’m sure it’s not done the Erlang way and that I’ve missed someway to make it more compact.  However, the code does work.

If you have a better solution, please submit it in the comments. If something can be improved, I always welcome knowing how.

Filed in: Languages,Programming.

14 Comments

  1. Comment by Mitchell Hashimoto:

    Looks good! The only thing I have to pick on is that the last statement N when true can simply be _ which will catch all 😉

    Also, I suppose since Erlang is functional, it would make more sense to implement this functionally:

    grades(N) when is_integer(N), N >= 90 -> “A”;
    grades(N) when is_integer(N), N >= 80 -> “B”;

    grades(N) when N =:= “A”; N =:= “a” -> “A”;

    grades(_) -> throw(“Not a valid grade.”).

    Also, in the case of being functional, the “catch all” wouldn’t be required since if its not there, Erlang will throw a badmatch exception anyways :)

    But all in all, fun use of Erlang.

    September 14, 2008 @ 21:12
  2. Comment by Vat:

    I haven’t compiled this (no erlang installed here) so it may not work, but this is kind of how i’d write it:

    — —

    grades( N ) when is_integer( N ) ->  grade( N );
    grades( S ) when is_list(N) ->  grade_str( S, ["a", "b", "c" "d","f"] ).
    
    grade( N ) when N >= 90 -> "A";
    grade( N ) when N >= 80 -> "B";
    grade( N ) when N >= 70 -> "C";
    grade( N ) when N >= 60 -> "D";
    grade( N ) when N >= 0 -> "F";
    grade( N ) when  N  throw("Not a valid grade").
    
    grade_str( S , [ Grade | Tail ] ) ->
      if 
          Grade == string:to_lower(S) -> string:to_upper( S );
          false -> grade_str(S, Tail )
      end.
    
    

    September 15, 2008 @ 03:25
  3. Comment by Philip Robinson:

    grades(N) when N == “A”; N == “a”; is_integer(N), N >= 90 -> “A”;
    grades(N) when N == “B”; N == “b”; is_integer(N), N >= 80 -> “B”;
    grades(N) when N == “C”; N == “c”; is_integer(N), N >= 70 -> “C”;
    grades(N) when N == “D”; N == “d”; is_integer(N), N >= 60 -> “D”;
    grades(N) when N == “E”; N == “e”; is_integer(N), N >= 0 -> “E”;
    grades(_) -> throw(“Not a valid grade”).

    The semi-colons in a guard sequence separate guards, each of which is one or more guard expressions separated by a comma.

    So the first line above is read as “when (N is equal to “A”) or (N is equal to “a”) or (N is an integer and N is greater than or equal to 90)”.

    September 15, 2008 @ 06:46
  4. Comment by Mark:

    Thanks for all the comments. I knew I was missing the best way to write this: functional is apparently the way to go from the responses so far.

    Sorry Steve for the formatting issues. I’ve never tested the preview plug in with code before – it obviously has some problems. I’ll make your code look nice once I have the chance, but right now it’s a busy day at work.

    EDIT 9/15: Steve’s previous comment has been deleted and his updated version is below with corrected code.

    Thanks again for all the feedback – I’m going to compile some of these and test them out for my knowledge.

    September 15, 2008 @ 08:24
  5. Comment by Kevin Smith:

    Here’s my crack at it with a more Erlang-y flavor:

    -module(grades).
    -compile([export_all]).
    -define(VALID_GRADES, ["A", "B", "C", "D", "F"]).
    
    grade(N) when is_integer(N), N >= 90 -> "A";
    grade(N) when is_integer(N), N >= 80 -> "B";
    grade(N) when is_integer(N), N >= 70 -> "C";
    grade(N) when is_integer(N), N >= 60 -> "D";
    grade(N) when is_integer(N), N >= 0 -> "F";
    grade(N) when is_list(N), length(N) == 1 -> Target = string:to_upper(N),
      case lists:member(Target, ?VALID_GRADES) of
        true -> Target;
        false -> throw(invalid_grade)
      end;
    grade(_) -> throw(invalid_grade).
    test() ->
      case
        grade(91) =:= "A" andalso
        grade(87) =:= "B" andalso
        grade(72) =:= "C" andalso
        grade(42) =:= "F" andalso
        grade("c") =:= "C" andalso
        grade("A") =:= "A" of
        true ->
          try
    	grades:grade("q"),
    	io:format("Tests fail...~n")
          catch
    	_ : invalid_grade ->
    	  io:format("Tests pass...~n")
          end;
        false ->
          io:format("Tests fail...~n")
      end.
    
    

    Looks like the formatting is off in your comments — indent as needed.

    September 15, 2008 @ 08:46
  6. Comment by Mark:

    All right, I think I’ve gotten everyone’s code formatted properly. This is a problem I’m going to have to look into more in the future, but for now, if I’ve missed something or miss-handled your code, let me know.

    Thanks.

    September 15, 2008 @ 09:04
  7. Comment by Steve Vinoski:

    Hi Mark, the code I submitted shows up completely wrong, and it won’t even compile if you cut and paste it. Can you either replace the previous comment or delete it, and let’s try again:

    -module(grades).
    -export([letter_grade/1]).

    letter_grade(N)
     when is_integer(N), N >= 0, N =< 100 ->
      L = ["F", "F", "F", "F", "F", "F", "D",
       "C", "B", "A", "A"],
      lists:nth(trunc(N/10)+1, L);
    letter_grade([G|_]=L)
     when length(L) == 1, G >= $A, G =< $F, G =/= $E ->
      L;
    letter_grade([G|_]=L)
     when length(L) == 1, G >= $a, G =< $f, G =/= $e ->
      string:to_upper(L);
    letter_grade(Grade) ->
      throw(
       lists:flatten(
        io_lib:format("~p: not a valid grade",
         [Grade]))).

    September 15, 2008 @ 09:57
  8. Comment by Mark:

    Steve, it’s been done. Sorry about the mess.

    September 15, 2008 @ 10:04
  9. Comment by Mitchell Hashimoto:

    Ah! I’m very glad that Kevin Smith included testing in his! :) That alone makes it my favorite.

    September 15, 2008 @ 10:25
  10. Comment by Steve Vinoski:

    @Mitchell: unfortunately the presence of the test code in the examples above is not enough to prevent bugs. What happens if you pass 101, for example? The originals in the other languages on Stuart’s page would call that an error. My code gets that case right.

    If it’s test code you want, here’s my version with the tests attached:

    -module(grades).
    -export([letter_grade/1, test/0]).

    letter_grade(N)
     when is_integer(N), N >= 0, N =< 100 ->
      L = ["F", "F", "F", "F", "F", "F", "D",
       "C", "B", "A", "A"],
      lists:nth(trunc(N/10)+1, L);
    letter_grade([G|_]=L)
     when length(L) == 1, G >= $A, G =< $F, G =/= $E ->
      L;
    letter_grade([G|_]=L)
     when length(L) == 1, G >= $a, G =< $f, G =/= $e ->
      string:to_upper(L);
    letter_grade(_) ->
      throw(invalid_grade).

    test() ->
      ok = check_grade("A", 90, 100),
      ok = check_grade("B", 80, 89),
      ok = check_grade("C", 70, 79),
      ok = check_grade("D", 60, 69),
      ok = check_grade("F", 0, 59),
      ok = try letter_grade(101)
       catch throw:invalid_grade -> ok;
        _:_ -> fail
       end,
      ok = try letter_grade(-1)
       catch throw:invalid_grade -> ok;
        _:_ -> fail
       end,
      ok = try letter_grade("E")
       catch throw:invalid_grade -> ok;
         _:_ -> fail
       end,
      ok = try letter_grade("Aa")
       catch throw:invalid_grade -> ok;
         _:_ -> fail
       end,
      ok = try letter_grade(a)
       catch throw:invalid_grade -> ok;
        _:_ -> fail
       end.

    check_grade(Grade, Start, End) ->
      lists:map(fun(G) -> Grade = letter_grade(G) end,
       lists:seq(Start, End)),
      ok.

    September 15, 2008 @ 15:08
  11. Comment by Kevin Smith:

    @Steve – You sir are correct. I should’ve read the problem more closely. I totally missed that. Ah well, I still think the code’s not too bad for only spending a few minutes on it.

    September 15, 2008 @ 19:06
  12. Comment by DSmith:

    A slight variation on a previous post…

    -module(grades).
    -export([grade/1]).

    grade(N) when is_integer(N), N >= 90 -> "A";
    grade(N) when is_integer(N), N >= 80 -> "B";
    grade(N) when is_integer(N), N >= 70 -> "C";
    grade(N) when is_integer(N), N >= 60 -> "D";
    grade(N) when is_integer(N), N >= 0 -> "F";
    grade([G]) when G >= $a, G = [G-32];
    grade([G]) when G >= $A, G = [G];
    grade(T) -> throw({invalid_grade, T}).

    September 15, 2008 @ 20:18
  13. Comment by Mark:

    @Kevin, @Steve,

    To defend Kevin, in my implementation I didn’t follow Stuart’s code exactly, and opted to allow grades over 100 and I also didn’t note the restriction in my description of the problem. What of the case where extra credit is awarded, so the grade is over 100?

    So based on the acceptance criteria, this either is, or is not a bug.

    Therefore, I think both implementations are equally valid.

    I do appreciate seeing the test code in Erlang. It is a topic that I think is not addressed often enough, how to apply TDD to Erlang.

    September 16, 2008 @ 08:51
  14. Comment by Ulf Wiger:

    Unfortunately, the test code is so verbose that one cannot bare to look at it (no offense, guys 😉

    Here’s a version together with a unit test using QuickCheck.
    It tests both valid and some invalid inputs
    (not sure how to get the formatting right, though):

    -module(grades).
    -export([grades/1, test/0]).
    -include_lib(“eqc/include/eqc.hrl”).

    -define(VALID_GRADES, “abcdfABCDF”).

    grades(N) when is_integer(N), N > 100 -> invalid;
    grades(N) when N == “A”; N == “a”; is_integer(N), N >= 90 -> “A”;
    grades(N) when N == “B”; N == “b”; is_integer(N), N >= 80 -> “B”;
    grades(N) when N == “C”; N == “c”; is_integer(N), N >= 70 -> “C”;
    grades(N) when N == “D”; N == “d”; is_integer(N), N >= 60 -> “D”;
    grades(N) when N == “F”; N == “f”; is_integer(N), N >= 0 -> “F”;
    grades(_) -> invalid.

    test() ->
    ?FORALL(
    V, oneof([valid,invalid]),
    ?LET(R, result(V),
    expected(V, catch grades(R), R))).

    result(valid) ->
    oneof([choose(0,100),
    [oneof(?VALID_GRADES)] % string of length 1
    ]);
    result(invalid) ->
    oneof([?LET(N, nat(), -(1+N)),
    ?LET(N, nat(), N + 101),
    [?SUCHTHAT(C,choose(0,255),
    not(lists:member(C,?VALID_GRADES)))] % string
    ]).

    expected(valid,invalid,_) -> false;
    expected(invalid,invalid,_) -> true;
    expected(valid,G,R) when is_integer(R) ->
    {Min,Max} = interval(G),
    R >= Min andalso R =< Max;
    expected(valid,G,R) ->
    string:to_upper(R) == G.

    interval(“A”) -> {90,100};
    interval(“B”) -> {80,89};
    interval(“C”) -> {70,79};
    interval(“D”) -> {60,69};
    interval(“F”) -> {0,59}.

    September 16, 2008 @ 11:22