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.
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.
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.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)”.
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.
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.
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.
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]))).
Steve, it’s been done. Sorry about the mess.
Ah! I’m very glad that Kevin Smith included testing in his!
That alone makes it my favorite.
@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.
@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.
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}).
@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.
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}.