22

While reading a snipped from Haskell for Great Good I found the following situation:

treeInsert :: (Ord a) => a -> Tree a -> Tree a  
treeInsert x EmptyTree = singleton x  
treeInsert x (Node a left right)   
  | x == a = Node x left right  
  | x < a  = Node a (treeInsert x left) right  
  | x > a  = Node a left (treeInsert x right)

Wouldn't it be better for performance if we just reused the given Tree when x == a?

treeInsert :: (Ord a) => a -> Tree a -> Tree a  
treeInsert x EmptyTree = singleton x  
treeInsert x all@(Node a left right)   
  | x == a = all  
  | x < a  = Node a (treeInsert x left) right  
  | otherwise  = Node a left (treeInsert x right)

In real life coding, what should I do? Are there any drawbacks when returning the same thing?

FtheBuilder
  • 1,410
  • 12
  • 19
  • 10
    Good question. My immediate reaction is "what does the Core say?" – MathematicalOrchid Oct 03 '15 at 12:41
  • 6
    Just a nit, for better performance I'd pattern match on `compare x a` to prevent the comparison/equality being called twice (or even three times). – Petr Oct 03 '15 at 12:54
  • @MathematicalOrchid, Thank you for your quick reply. Do you have any link where I could find more information when you mentioned the Core? – FtheBuilder Oct 03 '15 at 12:56
  • 2
    @FtheBuilder Basically, save just this one function as a module, compile with `-dump-simpl`, and then wade through the gigantic output this produces. In seriousness though: somebody more skilled at this will probably need to interpret the meaning of the results. – MathematicalOrchid Oct 03 '15 at 13:00
  • Considering that `left` and `right` are shared anyway, in the case `x == a`, I guess there is no noticeable difference, just a negligible constant factor for the one additional `Node` constructor per the whole operation. The situation would have been different if the whole sub-tree below that node were recomputed. – Petr Oct 03 '15 at 13:33
  • 1
    What if `x == a` does **not** imply that `f x == f a` for all functions `f`? Then you do not have the option to make that optimization. – behzad.nouri Oct 03 '15 at 13:57
  • 7
    @behzad.nouri - well, "==" should mean exactly that. if `x == a` does not imply `f x == f a` then either the author of that particular "==" instance did make a mistake, or someone is using unsafe functions. I think it's safe to assume that the implication holds. – Michael Oct 03 '15 at 14:07
  • 2
    @Michael not necessarily – behzad.nouri Oct 03 '15 at 14:07
  • 3
    @behzad.nouri - oh yes, defining broken `Eq` instances is just bad programming style, and nothing more. it's the same as defining a Monad that doesn't follow the monad laws. – Michael Oct 03 '15 at 14:09
  • 2
    @Michael what are the laws for `Eq` type class? – behzad.nouri Oct 03 '15 at 14:12
  • 5
    @behzad.nouri The law is that it should define "equality". And equality is defined in mathematics. (x == y) if and only if for every predicate P: ( P(x) = P(y) ). From that, it follows that (f x == f y) if you define Z = f x and P(a) := (a == Z). Or more simply, and less mathematically: `Eq` must be an equivalence relation, and if (x == y) then for all f it must be true that (f x == f y) – Michael Oct 03 '15 at 14:24
  • 1
    @behzad.nouri The defining property of equality is that two things which are equal may be substituted for each other in *all* circumstances (barring name capture). I think I've sometimes heard this called the "indistinguishability of identicals" law. What it means for two things to be equal is something that has been studied in detail for a very long time. [Homotopy type theory](https://en.wikipedia.org/wiki/Homotopy_type_theory) has some of the latest research in that general area (as well as some other concepts). – David Young Oct 03 '15 at 15:47
  • I remember reading something about `containers` or something making use of unsafe pointer equality as an optimization when doing this kind of thing. Anyone know what I'm thinking of? – jberryman Oct 03 '15 at 18:12
  • 1
    @DavidYoung, Michael, it's generally expected to be something like "externally observable equality". Most interesting data structures define `==` as an equivalence relation other than "true equality" but ensure that users of the public API cannot distinguish `==` values without unsafe tricks or functions explicitly documented as unsafe. For a priority queue, say, equality modulo tree balance (and `==` of the underlying elements) is the distinguished equivalence relation that in a HoTT setting would be defined to be equality more formally. – dfeuer Oct 03 '15 at 19:21
  • @dfeuer That is true, but if the internal structure isn't observable (like if it is hidden by the module) and the data structure is kept abstract, then `x = y` implies `f x = f y` for any `f` outside the module, since `f` wouldn't be able to see the any difference between the two values. – David Young Oct 03 '15 at 20:36
  • 2
    wouldn't something like "Should I avoid constructing ..." make a more appropriate title for this question? "instantiating" resonates with `instance Foo Bar where ...`. – Erik Kaplun Oct 04 '15 at 08:48
  • @ErikAllik good point, I've changed... – FtheBuilder Feb 15 '16 at 22:02

5 Answers5

5

Let's look at the core! (Without optimisations here)

$ ghc-7.8.2 -ddump-simpl wtmpf-file13495.hs

The relevant difference is that the first version (without all@(...)) has

                case GHC.Classes.> @ a_aUH $dOrd_aUV eta_B2 a1_aBQ
                of _ [Occ=Dead] {
                  GHC.Types.False ->
                    Control.Exception.Base.patError
                      @ (TreeInsert.Tree a_aUH)
                      "wtmpf-file13495.hs:(9,1)-(13,45)|function treeInsert"#;
                  GHC.Types.True ->
                    TreeInsert.Node
                      @ a_aUH
                      a1_aBQ
                      left_aBR
                      (TreeInsert.treeInsert @ a_aUH $dOrd_aUV eta_B2 right_aBS)

where reusing the node with that as-pattern does just

                TreeInsert.Node
                  @ a_aUI
                  a1_aBR
                  left_aBS
                  (TreeInsert.treeInsert @ a_aUI $dOrd_aUW eta_B2 right_aBT);

This is an avoided check that may well make a significant performance difference.

However, this difference has actually nothing to do with the as-pattern. It's just because your first snippet uses a x > a guard, which is not trivial. The second uses otherwise, which is optimised away.

If you change the first snippet to

treeInsert :: (Ord a) => a -> Tree a -> Tree a  
treeInsert x EmptyTree = singleton x  
treeInsert x (Node a left right)   
  | x == a     = Node x left right  
  | x < a      = Node a (treeInsert x left) right  
  | otherwise  = Node a left (treeInsert x right)

then the difference boils down to

      GHC.Types.True -> TreeInsert.Node @ a_aUH a1_aBQ left_aBR right_aBS

vs

      GHC.Types.True -> wild_Xa

Which is indeed just the difference of Node x left right vs all.

...without optimisations, that is. The versions diverge further when I turn on -O2. But I can't really make out how the performance would differ, there.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • 3
    This is because of `| otherwise` in the second version as opposed to `| x > a` in the first, which I think was not really what the question was intended to be about. – Reid Barton Oct 03 '15 at 14:04
  • I also don't find your new bolded assertion to be the case. The core I see with no optimizations is exactly what one would expect in all cases: GHC reuses the original node in the as-pattern, and uses `x` or `a` according to whether you wrote `x` or `a` in the non-as-pattern versions. GHC certainly will not assume that `x == a` implies that `x` and `a` are interchangeable when `x` and `a` are of an arbitrary type. – Reid Barton Oct 03 '15 at 14:54
  • @ReidBarton do the axioms for `Eq` require that `x` and `a` be interchangeable when `x == a`? I vaguely remember that some of the `Prelude`d functions assume that. – John Dvorak Oct 03 '15 at 16:27
  • 3
    `-dsuppress-all -dno-suppress-type-signatures` makes the core a whole lot more readable. – dfeuer Oct 03 '15 at 22:14
  • Do you have any tips for me to learn to interpret the core? It seems quite strange and difficult to figure out at first glance... (maybe a manual or something similar) – FtheBuilder Oct 06 '15 at 23:46
  • 1
    @FtheBuilder: well, I'm sure there's some literature (probably not a lot), but I've not read any. You can actually get quite a good feel for the core by toying around a bit with simple Haskell programs and their generated core. – leftaroundabout Oct 07 '15 at 00:08
4

In real life coding, what should I do? Are there any drawbacks when returning the same thing?

a == b does not guarantee that f a == f b for all functions f. So, you may have to return new object even if they compare equal.

In other words, it may not be feasible to change Node x left right to Node a left right or all when a == x regardless of performance gains.

For example you may have types which carry meta data. When you compare them for equality, you may only care about the values and ignore the meta data. But if you replace them just because they compare equal then you will loose the meta data.

newtype ValMeta a b = ValMeta (a, b)  -- value, along with meta data
    deriving (Show)

instance Eq a => Eq (ValMeta a b) where
    -- equality only compares values, ignores meta data
    ValMeta (a, b) == ValMeta (a', b') = a == a'

The point is Eq type-class only says that you may compare values for equality. It does not guarantee anything beyond that.

A real-world example where a == b does not guarantee that f a == f b is when you maintain a Set of unique values within a self-balancing tree. A self-balancing tree (such as Red-Black tree) has some guarantees about the structure of tree but the actual depth and structure depends on the order that the data were added to or removed from the set.

Now when you compare 2 sets for equality, you want to compare that values within the set are equal, not that the underlying trees have the same exact structure. But if you have a function such as depth which exposes the depth of underlying tree maintaining the set then you cannot guarantee that the depths are equal even if the sets compare equal.

Here is a video of great Philip Wadler realizing live and on-stage that many useful relations do not preserve equality (starting at 42min).

Edit: Example from ghc where a == b does not imply f a == f b:

\> import Data.Set
\> let a = fromList [1, 2, 3, 4, 5, 10, 9, 8, 7, 6]
\> let b = fromList [1..10]
\> let f = showTree
\> a == b
True
\> f a == f b
False

Another real-world example is hash-table. Two hash-tables are equal if and only if their key-value pairs tie out. However, the capacity of a hash-table, i.e. the number of keys you may add before having to re-allocate and rehash, depends on the order of inserts/deletes.

So if you have a function which returns the capacity of hash table, it may return different values for hash-tables a and b even though a == b.

behzad.nouri
  • 74,723
  • 18
  • 126
  • 124
  • 8
    in real life coding, you should never define an Eq instance that does not define equality in the mathematical sense. – Michael Oct 03 '15 at 14:08
  • 2
    Regardless of whether this is something you should ever do (I rather agree with Michael that you shouldn't), it's not actually so relevant to the question, because GHC seems to newly construct a `Node a left right` in either case. What's more relevant is that `not (a==b) && not (a>b)` doesn't imply `a – leftaroundabout Oct 03 '15 at 14:20
  • @leftaroundabout - I agree that it is better to use `otherwise`, because then you just see on first sight that the pattern is complete. However, for a valid `Ord` instance, the implication `not (a==b) && not (a>b)` implies `a a -> Tree a -> Tree a` says, "if you give me an a that is a proper Ord instance, I'll insert it into a Tree a. If you give me garbage, you may get garbage back. garbage in, garbage out ;)" – Michael Oct 03 '15 at 14:29
  • 1
    @Michael: I agree on that too, but [try `let n = log(-1) in n==n || nn`](https://en.wikipedia.org/wiki/NaN). (Arguably, it's the IEEE754 specification that's broken here, but GHC can't really just ignore this fact.) – leftaroundabout Oct 03 '15 at 14:44
  • even though it is more tedious to write, matching `compare a b` against `LT`, `EQ` and `GT` could be more efficient that testing `a < b` and `a == b`, especially if `a` and `b` are complex data structures. – ErikR Oct 03 '15 at 15:16
  • I don't think the video you've linked has anything to do with whether `a==b` ⟹̸ `f a==f b` is sensible. What Phil Wadler is talking about there is _free theorems for polymorphic functions_. The point is, for instance, that `a->a->Bool` is inhabited only by `\_ _ -> False` and `\_ _ -> True`, whereas `Eq a => a->a->Bool` also has `(==)` and `(/=)`. – leftaroundabout Oct 03 '15 at 15:41
  • 2
    "The point is Eq type-class only says that you may compare values for equality." What you describe after this part [is not equality](https://en.wikipedia.org/wiki/Equality_(mathematics)#Some_basic_logical_properties_of_equality). It would be an equivalence relation but not equality. We would get *very* few guarantees about what `(==)` means if we only required that it is an equivalence relation. It would then be completely valid to define `(==)` on `Int`s, for example, as congruence modulo 5. – David Young Oct 03 '15 at 16:03
  • 1
    @leftaroundabout, floating point representations have no business being in `Num`, `Enum`, `Eq`, or `Ord`. They belong in a tiny `FromRational` class along with proper rational numbers, but they are not numbers and they do not admit a notion of equality that is both useful and well-behaved. – dfeuer Oct 03 '15 at 19:40
  • @dfeuer: with this I don't quite agree. For `Enum` you're right [definitely](http://stackoverflow.com/questions/7290438/haskell-ranges-and-floats), and for `Eq` there's also [a big point against](http://www.paultaylor.eu/ASD/) (apart from the one discussed here). However, losing `Num` would be way too inconvenient, and there's IMO almost nothing wrong with that instance. Also not with `Fractional` and `Floating`, and `>` `<` are pretty much ok too (except in that `Eq` is a superclass of `Ord`). – leftaroundabout Oct 03 '15 at 23:13
  • 1
    @leftaroundabout, I expect numbers to obey the (semi)ring axioms. I expect fractions to obey the field axioms. Floating point representations don't even form semigroups. – dfeuer Oct 03 '15 at 23:57
  • 1
    @leftaroundabout : the `log(-1)` thing is really a bug imho. consider that `let n = log(-1) in compare n n` results in `GT`. – Michael Oct 04 '15 at 06:21
3

My two cents... perhaps not even about the original question:

Instead of writing guards with x < a and x == a, I would match compare a b against LT, EQ and GT, e.g.:

treeInsert x all@(Node a left right) =
  case compare x a of
    EQ -> ...
    LT -> ...
    GT -> ...

I would do this especially if x and a can be complex data structures, since a test like x < a could be expensive.

Franky
  • 2,339
  • 2
  • 18
  • 27
ErikR
  • 51,541
  • 9
  • 73
  • 124
  • 3
    Shouldn't this be a comment under the question rather than posted as an answer? – John Dvorak Oct 03 '15 at 16:32
  • 2
    I felt I needed more space than the comment area provides. Moreover the comment section under the original question was already very cluttered with another thread. Feel free to down vote (or non-vote) as you like. I contributed it because I considered it relevant to the question. – ErikR Oct 03 '15 at 16:45
  • Since the question is about 'real life coding' and about squeezing a tiny drop of performance out of your code, I'd say this answer is extremely relevant (this may increase performance two to three fold, depending on how expensive `==`, `<` and `>` are). – user2407038 Oct 04 '15 at 08:01
2

answer seems to be wrong. I just leave it here, for reference...


With your second function you avoid creating a new node, because the compiler cannot really understand equality (== is just some function.) If you change the first version to

-- version C
treeInsert :: (Ord a) => a -> Tree a -> Tree a  
treeInsert x EmptyTree = singleton x  
treeInsert x (Node a left right)   
  | x == a = Node a left right  -- Difference here! Changed x to a.
  | x < a  = Node a (treeInsert x left) right  
  | x > a  = Node a left (treeInsert x right)

the compiler will probably be able to do common subexpression elimination, because the optimizer will be able to see that Node a left right is the same as Node a left right.

On the other hand, I doubt that the compiler can deduce from a == x that Node a left right is the same as Node x left right.

So, I'm pretty sure that under -O2, version B and version C are the same, but version A is probably slower because it does an extra instantiation in the a == x case.

Michael
  • 6,451
  • 5
  • 31
  • 53
  • “So, I'm pretty sure that under `-O2`, version B and version C are the same” – I can't confirm that. Both A and C perform an extra check not needed in B (in GHC-7.8.2 or GHC-7.10.1). – leftaroundabout Oct 03 '15 at 13:54
  • oh, the compiler is not as smart as I thought it was – Michael Oct 03 '15 at 13:56
  • Wait a moment, I think the difference has nothing to do with the reconstructed node at all... it's just that the `otherwise` guard is faster than the `x > a`. – leftaroundabout Oct 03 '15 at 14:09
2

Well, if the first case had used a instead of x as follows, then there's at least the chance that GHC would eliminate the allocation of a new node through common subexpression elimination.

treeInsert x (Node a left right)   
  | x == a = Node a left right

However, this is all but irrelevant in any non-trivial use case, because the path down the tree to the node is going to be duplicated even when the element already exists. And this path is going to be significantly longer than a single node unless your use case is trivial.

In the world of ML, the fairly idiomatic way to avoid this is to throw a KeyAlreadyExists exception, and then catch that exception at the top-level insertion function and return the original tree. This would cause the stack to be unwound instead of allocating any of the Nodes on the heap.

A direct implementation of the ML idiom is basically a no-no in Haskell, for good reasons. If avoiding this duplication matters, the simplest and possibly best thing to do is to check if the tree contains the key before you insert it.

The downside of this approach, compared to a direct Haskell insert or the ML idiom, is that it involves two traversals of the path instead of one. Now, here is a non-duplicating, single-pass insert you can implement in Haskell:

treeInsert :: Ord a => a -> Tree a -> Tree a
treeInsert x original_tree = result_tree
  where
    (result_tree, new_tree) = loop x original_tree

    loop x EmptyTree = (new_tree, singleton x)
    loop x (Node a left right) =
       case compare x a of
         LT -> let (res, new_left) = loop x left
                in (res, Node a new_left right)
         EQ -> (original_tree, error "unreachable")
         GT -> let (res, new_right) = loop x right
                in (res, Node a left new_right)

However, older versions of GHC (roughly 7-10 years ago) don't handle this sort of recursion through lazy pairs of results very efficiently, and in my experience check-before-insert is likely to perform better. I'd be slightly surprised if this observation has really changed in the context of more recent GHC versions.

One can certainly imagine a function that directly constructs (but does not return) a new path for the tree, and decides to return the new path or the original path once it's known whether the element exists already. (The new path would immediately become garbage if it is not returned.) This conforms to the basic principles of the GHC runtime, but isn't really expressible in the source language.

Of course, any completely non-duplicating insertion function on a lazy data structure is going to have different strictness properties than a simple, duplicating insert. So no matter the implementation technique, they are different functions if laziness matters.

But of course, whether or not the path is duplicated may not matter that much. The cases where it would matter the most would be when you are using the tree persistently, because in linear use cases the old path would become garbage immediately after each insertion. And of course, this only matters when you are inserting a significant number of duplicates.

lpsmith
  • 707
  • 3
  • 8