top | item 8005156

Option and Null in Dynamic Languages

37 points| rntz | 11 years ago |rntz.net | reply

85 comments

order
[+] lispm|11 years ago|reply
Common Lisp can return multiple values from functions. Thus it returns two values from a hash-table lookup:

    CL-USER 45 > (gethash 'foo (make-hash-table))
    NIL
    NIL
The first value is the stored value in the hash table. If there is no key, the first value is NIL. The second value indicates whether the key is present in the hash table.
[+] rntz|11 years ago|reply
I list Common Lisp & its behavior in Appendix C at the end of the article. Multiple-return-value is definitely better than just returning NIL, but I still dislike it; it doesn't actively prevent you from confusing NIL with absence, so "the obvious way to do it" can still be the wrong way. A good CL programmer won't fall into this trap, but the article is in large part about how some languages/interfaces default you into good programming by requiring you to distinguish success from failure, and others don't.

Another way of seeing the same thing is: Type-theoretically speaking, MRV is using a product type (two values) to emulate a sum type (one value of two possible forms); why not just use a sum type?

[+] derefr|11 years ago|reply
And in Erlang:

    1> X = #{"a" => 1}.
    #{"a" => 1}
    2> maps:find("a", X).
    {ok,1}
    3> maps:find("x", X).
    error
Constructing and destructuring tuples is cheap in the Erlang VM, so Erlang uses them where other languages would use types.

Further, in idiomatic Erlang, you don’t bother with a branch for each case of the option type; you just do the following:

    {ok, Val} = maps:find("a", X).
Effectively, that's a type assertion: if maps:find/1 responds with error instead of {ok, _}, that’s a process crash.
[+] ataggart|11 years ago|reply
Correctness can often arise naturally when the implementation reflects the function's intent by composing "correct" functions. E.g.,

  (defn values-in [m ks]
    (vals (select-keys m ks)))
[+] dkersten|11 years ago|reply
Furthermore, I've found that absent keys degrading to nil is actually a very useful feature that makes a lot of code really composable and simple. I can't think of any Clojure code I've written recently where a value of nil actually should be distinguished from an absent value - I just write my code so that semantically they mean the same thing, because this makes the code much easier to compose and much easier to think about.
[+] typomatic|11 years ago|reply
A more sane* way to implement the single lookup behavior is:

  def values_in(keys, d):
      ret = {}
      for key in keys:
          try:
              ret[key] = d[key]
          except KeyError:
              pass
      return ret
which works much more quickly in cases where keys is nearly all of d.keys, and doesn't force you to reinvent monads in Python, which is arguably unsuited for them.

* Sanity values may vary for people who haven't gotten into the habit of using Python exceptions for little things.

[+] dragonwriter|11 years ago|reply
The article is looking at returning a list, while your option returns a dict, but that's an easy fix. And even for an Option-like approach, the article tries way too hard: Option can be viewed as a constrained cardinality list, and regular lists work just fine to implement them.

  from itertools import chain
  
  def getOpt(d, k):
      return [d[key]] if key in d else []

  def values_in(keys, d):
      return list(chain(*[ getOpt(d,key) 
                           for key in keys ]))
Or Ruby:

  def values_in(keys, d)
    keys.flat_map {|k| d.has_key?(k) ? [d[k]] : []}
  end
[+] anon4|11 years ago|reply
As long as we're playing code golf, try this for python:

  def values_in(dict, keys):
      marker = object()
      return [x
              for x in (dict.get(k, marker)
                        for k in keys)
              if x is not marker]
[+] tieTYT|11 years ago|reply
> In Lua, it's impossible to have a dictionary that maps a key to nil: setting a key to nil removes it from the dictionary!

Wait is he really trying to say, "Setting a VALUE to nil removes the entry from the dictionary!"? If so, that sounds terrible.

I can imagine barking up the wrong tree wondering why my map is missing fields when the real problem is an object I passed into it is unexpectedly nil.

[+] russellsprouts|11 years ago|reply
Yes, that's true. It does have drawbacks, but overall I think it is better than say, JavaScript. In JavaScript, a key can be not present in an object, be undefined, be null, be set to some other value, or be present in the prototype. The difference between these can cause very subtle bugs.

        //Let's define some object
        var obj = {a: true, b: true, c: true, d: true, hasOwnProperty: true}

        //Now let's make them falsy in different ways
        obj.a = undefined; //set to undefined
        obj.b = null; //set to null
        obj.c = false; //set to false
        delete obj.d; //delete key
        delete obj.hasOwnProperty //delete key, but present in prototype

        console.log(obj.a) //--> undefined
        console.log(obj.b) //--> null
        console.log(obj.c) //--> false
        console.log(obj.d) //--> undefined
        console.log(obj.hasOwnProperty) //--> [Function]

        console.log('a' in obj) //--> true
        console.log('b' in obj) //--> true
        console.log('c' in obj) //--> true
        console.log('d' in obj) //--> false
        console.log('hasOwnProperty' in obj) //--> true

        console.log(obj.a == null) //--> true
        console.log(obj.b == null) //--> true
        console.log(obj.c == null) //--> false
        console.log(obj.d == null) //--> true
        console.log(obj.hasOwnProperty == null) //--> false 

        for(var key in obj) {
          console.log(key) //--> prints a, b, c
        }
In Lua, the only data structure is a hashtable, which maps any non-nil value to any other non-nil value. It can also be set in the objects __index metamethod/table

        local tab = {a = true, b = true, c = true}
        
        -- by default there are none, but we can defined a fallback metatable similar to a prototype method like js's obj.prototype.hasOwnProperty

        setmetatable(tab, {__index={c='meta c', d='meta d'}})

        -- if it is important that a key is explicitly set to false, do so, otherwise simply remove it
        tab.a = false --set to false
        tab.b = nil --set to nil
        tab.c = nil --set to nil, present in fallback table
        tab.d = false --set to false, present in fallback table

        print(tab.a) --false
        print(tab.b) --nil
        print(tab.c) --meta c
        print(tab.d) --false
The rules for Lua are simple: Look for the key in the table, if it's not there, check the fallback defined in the metatable, if not, then return nil.

This is especially important in Lua, because any value can be a key, you would have to be sure to delete the key otherwise it could not be garbage collected. local tab1 = {1,2,3,4,5} local tab2 = {} tab2[tab1] = true for k,v in pairs(tab2) do tab[k] = nil end

If this worked like JavaScript, then tab2[tab1] would return nil, but tab1 could not be garbage collected until tab2 is. Because JavaScript objects can only have strings for keys, this is less of an issue.

[+] rntz|11 years ago|reply
Yes, that is what I meant. And yeah, it is weird behavior. (I think of "d[k] = v" as "setting the key k to the value v", so that's why I said "setting a key". Perhaps "binding a key" would be clearer?)
[+] wfjackson|11 years ago|reply
Null, empty string, empty value, Undefined etc. are the biggest causes of crashes,edge case bugs and a big time sink programming and data handling. Wish languages had better support to handle straightforward cases instead of just crashing.
[+] wvenable|11 years ago|reply
Crashing is ok. The problem is that it usually doesn't crash in the right spot. A null is generated somewhere else and propagated around for a while and the crash happens later in a unrelated area. But it's even worse if it doesn't crash at all.
[+] taeric|11 years ago|reply
This is close to saying that lack of oxygen is the number one killer in drownings. That is, when you are in that situation there is a lot going wrong.

So, sure, you got rid of null from the language. Does your program handle the case where it was handed a negative number correctly? All positive values? All unicode?

Consider, in the Ariane Rocket incident, they were using a dependently typed language. Didn't magically prevent errors in assembly.

So, fine, we'll make our programs where they handle all cases... Rapid prototyping probably just flew out the window. As did worrying about actual honest to goodness use cases.

A great quote by Knuth I saw once was where he acknowledged that tex would choke on mile wide documents. But who cares?