2

A classic case of inefficiency in functional program is things like the reverse function written from specification

import Prelude()

[] ++ ys = ys
(x:xs) ++ ys = x:(xs ++ ys)

reverse []     = []
reverse (x:xs) = (reverse xs) ++ [x]

Using associativity, one can derive an efficient reverse

-------- REWRITE ------

-- RULE : ++ relation (deduced)
-- reverse xs = reverse xs ++ []

-- RULE : [] relation -- case split
-- reverse []      = reverse [] ++ []
-- reverse (x::xs) = reverse (x::xs) ++ [] 

-- RULE : reverse relation 
-- reverse []      = []                
-- reverse (x::xs) = (reverse xs ++ [x]) ++ []

-- RULE : ++ relation (deduced)
-- reverse []      = []                
-- reverse (x::xs) = reverse xs ++ ([x] ++ [])

-- RULE : ++ relation
-- reverse []      = []                
-- reverse (x::xs) = reverse xs ++ (x::[])

-- RULE :  (language)
-- reverse' []      _   = []                
-- reverse' (x::xs) acc = reverse xs ++ acc
-- reverse  xs = reverse' xs []

reverse_eff xs = reverse' xs []
  where 
    reverse' [] ys  = ys
    reverse' (x:xs) ys  = reverse' xs (x:ys)

However, where is the problem coming from in the first place ?

  • We instruct a specific order for ++
  • Composition as defined by our language defines an order for composition of our ++
  • Our language is unaware of laws, derived from the code itself, and is unaware of ideas of efficiency.

What we really meant to do was :

  • specify a (functional) relation ++, properly quotiented by all its laws
  • use relational composition for it, which hides which specific parenthesizing will be used
  • at usage site, select one parenthesizing which best fit our access pattern and goal (in the case of reverse, this means shifting all the parenthesis to match the way the resulting list is constructed)

Are there any language which generates some laws, and searches for the best rewrite according to some metric ?

nicolas
  • 9,549
  • 3
  • 39
  • 83
  • 2
    I'd like to point out that going from `reverse` to `reverse_eff` is not just searching for a rewrite, but searching for a modification to the program text whose effect on each output is the desired rewrite. That seems much harder. I don't know whether any languages have tried to do that well, but... well, good luck. – Daniel Wagner Apr 06 '21 at 06:54
  • @DanielWagner I didn't mean it as textual program for the user to see, but internally. But I don't think that part is too hard : I would expect liquid haskell proofs could be reified as a haskell type + term, for instance. it's just going from curry to church type of proofs. (categorically that's a "comprehension"). – nicolas Apr 06 '21 at 07:32
  • The problem in general with applying laws is that the compiler can't prove the laws hold. That's made worse in Haskell by the ever-present possibility of bottom. BTW I think your `case split` rewrite has a typo? With `reverse`, the consumer of the reversed list might lazily ignore some/most of it -- then reversing all of it was unnecessary, no matter how 'efficient'. – AntC Apr 06 '21 at 09:51
  • Anyhoo, lists are a notoriously inefficient way of representing anything. You're likely to get better whole-of-program efficiencies by (say) using a BTree. – AntC Apr 06 '21 at 09:52
  • (thks for the typo). liquid haskell lifts haskell program to the logical level. which itself can be "downed" back to haskell. Z3 can back search for laws (like F*, or liquid haskell), I imagine it's feasible to generate them. what I don't know is proof system that track and minimize a metric. in which case, change of representation like BTree would naturally be selected where it matters. it just boils down to an optimisation problem. – nicolas Apr 06 '21 at 11:51
  • what about Maude http://maude.cs.illinois.edu/w/index.php/The_Maude_System – PF. Castro Apr 06 '21 at 13:04
  • 1
    "change of representation ... would naturally be selected" that's the dream and the original promise of abstract data types in FP (not just algebraic which freeze in the implementational details much too early). if that promise were to be fulfilled we'd just program everything to lists and have the efficient array or what have you implementation derived by the compiler, according to the specific program. consider `main = print . reverse . reverse $ "Hi!"` though. true efficiency comes with delaying the specific implementations as much as possible, and only registering the intent of the calls. – Will Ness Apr 06 '21 at 14:57
  • @WillNess indeed, type driven selection of typeclass instances is a powerful, type-level, example of that delay, relying on the hard phase separation between compile and runtime, with no optimization and a unique prooftree. on the other hand the language itself is ordinarily lifted at logical level in Fstar/liquid haskell to find a prooftree. I wonder what's missing to start optimizing a metric and compare the value of different prooftrees, using those packed by each representation (List, btree) along with their generated equalities/invariants. what exists between nothing and the pipe dream? – nicolas Apr 07 '21 at 13:07

0 Answers0