haskell -- set fixedpoint library?
Asked Answered
J

1

6

I'm looking for a library that will compute the fixed point / closure of a set under a number of operators of variable arity. For example,

fixwith [(+)] [1]

for the integers should compute all of N (the naturals, 1..). I tried taking a stab at writing it, but some things are lacking. It's not very efficient, and I have a feeling that my handling of multi-arity functions is not the most elegant. Further, would it be possible to write using the builtin fix function instead of manual recursion?

class OperatorN α β | β -> α where
    wrap_op :: β -> (Int, [α] -> α)

instance OperatorN α (() -> α) where
    wrap_op f = (0, \[] -> f ())

instance OperatorN α (α -> α) where
    wrap_op f = (1, \[x] -> f x)

instance OperatorN α ((α, α) -> α) where
    wrap_op f = (2, \[x, y] -> f (x, y))

instance OperatorN α ((α, α, α) -> α) where
    wrap_op f = (3, \[x, y, z] -> f (x, y, z))

instance OperatorN α ((α, α, α, α) -> α) where
    wrap_op f = (4, \[x, y, z, w] -> f (x, y, z, w))

type WrappedOp α = (Int, [α] -> α)
fixwith_next :: Eq α => [WrappedOp α] -> [α] -> [α]
fixwith_next ops s = List.nub (foldl (++) s (map g ops)) where
    g (0, f) = [f []]
    g (arity, f) = do
        x <- s
        let fx = \xs -> f (x:xs)
        g (arity - 1, fx)
fixwith ops s
    | next <- fixwith_next ops s
    , next /= s
    = fixwith ops next
fixwith _ s = s

examples,

> fixwith [wrap_op $ uncurry (*)] [-1 :: Int]
[-1,1]
> fixwith [wrap_op $ uncurry (*)] [1 :: Int]
[1]
> fixwith [wrap_op $ max 3, wrap_op $ \() -> 0] [1 :: Int]
[1,3,0]

set version

This doesn't improve performance all that much, though I guess I just need to figure out how to do less computation to make it actually faster.

import qualified Control.RMonad as RMonad

class OperatorN α β | β -> α where
    wrap_op :: β -> (Int, [α] -> α)

instance OperatorN α (() -> α) where
    wrap_op f = (0, \[] -> f ())

instance OperatorN α (α -> α) where
    wrap_op f = (1, \[x] -> f x)

instance OperatorN α ((α, α) -> α) where
    wrap_op f = (2, \[x, y] -> f (x, y))

instance OperatorN α ((α, α, α) -> α) where
    wrap_op f = (3, \[x, y, z] -> f (x, y, z))

instance OperatorN α ((α, α, α, α) -> α) where
    wrap_op f = (4, \[x, y, z, w] -> f (x, y, z, w))

type WrappedOp α = (Int, [α] -> α)

fixwith_next :: Ord α => [WrappedOp α] -> Set α -> Set α
fixwith_next ops s = Set.unions $ s : map g ops where
    g (0, f) = RMonad.return $ f []
    g (arity, f) = s RMonad.>>= \x ->
        g (arity - 1, \xs -> f (x:xs))
fixwith' ops s
    | next <- fixwith_next ops s
    , next /= s
    = fixwith' ops next
fixwith' _ s = s
fixwith ops s = Set.toList $ fixwith' ops (Set.fromList s)

set version that's lazy

I used RMonad to clean this up a little, and made it lazy as Daniel suggested. I think most of the time is being spent in the actual multiplication routines, sadly, so I didn't see any performance benefit from this change. The laziness is cool though.

notin :: Ord α => Set α -> Set α -> Set α
notin = flip Set.difference

class Ord α => OperatorN α β | β -> α where
    next_values :: β -> Set α -> Set α

instance Ord α => OperatorN α (α -> α) where
    next_values f s = notin s $ s RMonad.>>= \x -> RMonad.return (f x)

instance Ord α => OperatorN α (α -> α -> α) where
    next_values f s = s RMonad.>>= \x -> next_values (f x) s

instance Ord α => OperatorN α (α -> α -> α -> α) where
    next_values f s = s RMonad.>>= \x -> next_values (f x) s

instance Ord α => OperatorN α (α -> α -> α -> α -> α) where
    next_values f s = s RMonad.>>= \x -> next_values (f x) s

-- bind lambdas with next_values
fixwith_next :: Ord α => [Set α -> Set α] -> Set α -> Set α
fixwith_next nv_bnd s = Set.unions $ map (\f -> f s) nv_bnd -- bound next values

fixwith' :: Ord α => [Set α -> Set α] -> Set α -> [α]
fixwith' ops s@(fixwith_next ops -> next)
    | Set.size next == 0 = []
    | otherwise = (Set.toList next) ++ fixwith' ops (Set.union s next)
fixwith ops s = (Set.toList s) ++ fixwith' ops s
fixwith_lst ops = fixwith ops . Set.fromList

example

> take 3 $ fixwith [next_values (+2)] (Set.fromList [1])
[1,3,5]

I had to lose unary operations, but that's not a deal killer.

Jimmie answered 30/8, 2011 at 23:12 Comment(0)
F
1

Nope, fix is a red herring. It's computing a different kind of fixed-point than you are.

Your handling of arity is very pragmatic. There are a number of different ways you could make it a bit less boiler-platey; see one of my previous answers for one such way. I'm sure someone will come on and add another mind-blowing type-level numerals-based solution eventually, as well. =)

For efficiency, I'm not sure you can do much better with only an Eq instance anyway. You might consider filtering out s values from the results of the calls to the (local) g function -- that is, letting fixwith_next return only the new elements. This ought to make the termination check faster and may even make it possible to have a productive, lazy fixwith.

If you're alright with strictness and requiring an Ord instance, using real Sets will probably improve the efficiency as well.

Font answered 31/8, 2011 at 2:22 Comment(2)
could you elaborate? it superficially seemed to me like if you could avoid it hanging, e.g. by knowing the number of elements, you could write take 2 $ fix (\x -> List.nub $ [1, 2] ++ x).Jimmie
You can use fix, but it won't be an interesting use of fix. In Haskell, fix is equivalent to recursion -- you can make any recursive definition non-recursive by calling fix, and you can take any call to fix and replace it by a recursive definition. So, the question "Can I use fix when defining this particular function?" is equivalent to the question "Can I use recursion when defining this particular function?". The answer is yes, but the question is silly, because writing the function is the real goal, and whether or not you use recursion is incidental.Font

© 2022 - 2024 — McMap. All rights reserved.