Exceptions- Lazy Sequences- and LSP in Clojure
04 Dec 2012
This is Exceptional! A violation of LSP with lazy sequences in Clojure.
Today I was working on my command parser in clojure. I was working on being able to pass multiple messages into a function in the form of a string and I was looking to get back a list of two element lists, each containing an id and a message. I could successfully get the format that I wanted with a single message, so I used that function to do most of the work for me in the function that I was creating to something similar with multiples. I had these tests in place.
1 (describe "decoding multiple structured messages" 2 3 (it "should return a list with two element lists that contain an id and a message when it receives a string" 4 (should= '((5 "cool")(7 "sweet")(12 "what")(14 "OMG")) (decode-structured-messages "5, cool; 7, sweet; 12, what; 14, OMG;"))) 5 6 (it "should return 'The number needs to be within range of 0 - 255.' if the number is out of range." 7 (should-throw Exception (decode-structured-messages "478, cool;"))) 8 9 (it "should throw an exception when passed something that is NAN as the command id." 10 (should-throw Exception (decode-structured-messages "a, super cool time;"))) 11 )
Here is the function that will parse the single message...
1 (defn decode-structured-message [structured-message] 2 (let [stripped-message (replace structured-message #";" "") ] 3 (let [id (read-string (first(split stripped-message #", "))) message (first (rest (split stripped-message #", ")))] 4 (cond 5 (not(integer? id))(throw (Exception. "The command id needs to be a number.")) 6 (or(< id 0) (> id 255))(throw (Exception. "The number needs to be within range of 0 - 255.")) 7 (and(>= id 0) (<= id 255)) (list id message)))))
I could successfully get the format that I wanted and the first test would pass with this code...
1 2 (defn decode-structured-messages [input] 3 (map decode-structured-message (split input #";")))
But Houston, we have a problem. My exceptions are not being thrown. I would tweak my code and then I could get the exceptions to pass but then my format was off and my first test was failing. What is going on here!!!!!!!!??????????
Well, let me tell you.
It turns out that we have a Liskov substitution principle violation. -Kind of. You see the Liskov substitution principle states (from wikipedia)
It states that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.)
LSP usually refers to mutable objects but is applied to object oriented programming and when thinking about programming in this type of way. This principle is also often thought about by programmers when using functional languages. 'How is that effecting me?', You ask? Let me tell you. While using Clojure, you likely use lazy sequences to stand in for lists. You likely think that lazy sequences and lists can be thought of as the same thing. There is a BIG difference(by design of course). Unlike lists, lazy sequences will only be invoked when they are called. In my method for decoding structured messages, I am using map, which returns a lazy sequence, but it turns out, I am never actually calling map so my tests on exceptions fail. Now that we have that part worked out, you may be thinking 'Then why does the first test pass if we are not calling map'? I was. Let me drop a bomb of knowledge on you.
In my tests, the first one in particular, when I use should, I use should=. Should= calls map. When I test for exceptions, I use should-throw. Should-throw never calls map and will be false since no exception is being thrown. I know of two work arounds, one of which I have currently added into my code. I would be open to and love some suggestions for a different or better solution for this situation if there is one. I will check into this and update with my findings.
Solution One - In my tests I can wrap first around my call to create-structured-messages. What I don't like about this solution is that then I am putting the solution into the test and that really does not fix the code and then if someone were using this code in the future, they could end up with some strange results.
1 (it "should return 'The number needs to be within range of 0 - 255.' if the number is out of range." 2 (should-throw Exception (first(decode-structured-messages "478, cool;"))))
Solution Two - In my code I can get the result from map, count it, and then take that many so that map will be called and I will get back everything. Although there may be a better way, at this point I am not aware of it. I like that with this solution you can do not have to modify the test and you can look right to the function to see what is happening.
1 (defn decode-structured-messages [input] 2 (let [result (map decode-structured-message (split input #";"))] 3 (take (count result) result)))
Although really frustrated by this while it was happening, I really think it was a great learning experience in the way of assumptions, the way that "should" works, lazy sequences, exceptions, and the way these parts work together.
Right now I feel like a peaceful stream.