jdm
jdm

Reputation: 10070

Is there a language with higher order conditionals?

Sometimes, I have a control structure (if, for, ...), and depending on a condition I either want to use the control structure, or only execute the body. As a simple example, I can do the following in C, but it's pretty ugly:

#ifdef APPLY_FILTER
if (filter()) {
#endif
    // do something
#ifdef APPLY_FILTER
}
#endif

Also it doesn't work if I only know apply_filter at runtime. Of course, in this case I can just change the code to:

if (apply_filter && filter())

but that doesn't work in the general case of arbitrary control structures. (I don't have a nice example at hand, but recently I had some code that would have benefited a lot from a feature like this.)

Is there any langugage where I can apply conditions to control structures, i.e. have higher-order conditionals? In pseudocode, the above example would be:

<if apply_filter>
if (filter()) {
    //  ...
}

Or a more complicated example, if a varable is set wrap code in a function and start it as a thread:

<if (run_on_thread)>
  void thread() {
<endif>

  for (int i = 0; i < 10; i++) {
      printf("%d\n", i);
      sleep(1);
  }

<if (run_on_thread)>
  }
  start_thread(&thread);
<endif>

(Actually, in this example I could imagine it would even be useful to give the meta condition a name, to ensure that the top and bottom s are in sync.)

I could imagine something like this is a feature in LISP, right?

Upvotes: 1

Views: 113

Answers (2)

Antal Spector-Zabusky
Antal Spector-Zabusky

Reputation: 36622

Any language with first-class functions can pull this off. In fact, your use of "higher-order" is telling; the necessary abstraction will indeed be a higher-order function. The idea is to write a function applyIf which takes a boolean (enabled/disabled), a control-flow operator (really, just a function), and a block of code (any value in the domain of the function); then, if the boolean is true, the operator/function is applied to the block/value, and otherwise the block/value is just run/returned. This will be a lot clearer in code.

In Haskell, for instance, this pattern would be, without an explicit applyIf, written as:

example1 = (if applyFilter then when someFilter else id) body
example2 = (if runOnThread then (void . forkIO) else id) . forM_ [1..10] $ \i ->
             print i >> threadDelay 1000000 -- threadDelay takes microseconds

Here, id is just the identity function \x -> x; it always returns its argument. Thus, (if cond then f else id) x is the same as f x if cond == True, and is the same as id x otherwise; and of course, id x is the same as x.

Then you could factor this pattern out into our applyIf combinator:

applyIf :: Bool -> (a -> a) -> a -> a
applyIf True  f x = f x
applyIf False _ x = x
-- Or, how I'd probably actually write it:
--     applyIf True  = id
--     applyIf False = flip const
-- Note that `flip f a b = f b a` and `const a _ = a`, so
-- `flip const = \_ a -> a` returns its second argument.

example1' = applyIf applyFilter (when someFilter) body
example2' = applyIf runOnThread (void . forkIO) . forM_ [1..10] $ \i ->
              print i >> threadDelay 1000000

And then, of course, if some particular use of applyIf was a common pattern in your application, you could abstract over it:

-- Runs its argument on a separate thread if the application is configured to
-- run on more than one thread.
possiblyThreaded action = do
  multithreaded <- (> 1) . numberOfThreads <$> getConfig
  applyIf multithreaded (void . forkIO) action

example2'' = possiblyThreaded . forM_ [1..10] $ \i ->
               print i >> threadDelay 1000000

As mentioned above, Haskell is certainly not alone in being able to express this idea. For instance, here's a translation into Ruby, with the caveat that my Ruby is very rusty, so this is likely to be unidiomatic. (I welcome suggestions on how to improve it.)

def apply_if(use_function, f, &block)
  use_function ? f.call(&block) : yield
end

def example1a
  do_when = lambda { |&block| if some_filter then block.call() end }
  apply_if(apply_filter, do_when) { puts "Hello, world!" }
end

def example2a
  apply_if(run_on_thread, Thread.method(:new)) do
    (1..10).each { |i| puts i; sleep 1 }
  end
end

def possibly_threaded(&block)
  apply_if(app_config.number_of_threads > 1, Thread.method(:new), &block)
end

def example2b
  possibly_threaded do
    (1..10).each { |i| puts i; sleep 1 }
  end
end

The point is the same—we wrap up the maybe-do-this-thing logic in its own function, and then apply that to the relevant block of code.

Note that this function is actually more general than just working on code blocks (as the Haskell type signature expresses); you can also, for instance, write abs n = applyIf (n < 0) negate n to implement the absolute value function. The key is to realize that code blocks themselves can be abstracted over, so things like if statements and for loops can just be functions. And we already know how to compose functions!

Also, all of the code above compiles and/or runs, but you'll need some imports and definitions. For the Haskell examples, you'll need the impots

import Control.Applicative -- for (<$>)
import Control.Monad       -- for when, void, and forM_
import Control.Concurrent  -- for forkIO and threadDelay

along with some bogus definitions of applyFilter, someFilter, body, runOnThread, numberOfThreads, and getConfig:

applyFilter     = False
someFilter      = False
body            = putStrLn "Hello, world!"
runOnThread     = True
getConfig       = return 4 :: IO Int
numberOfThreads = id

For the Ruby examples, you'll need no imports and the following analogous bogus definitions:

def apply_filter;  false; end
def some_filter;   false; end
def run_on_thread; true;  end
class AppConfig
  attr_accessor :number_of_threads
  def initialize(n)
    @number_of_threads = n
  end
end
def app_config; AppConfig.new(4); end

Upvotes: 6

Emil Vikstr&#246;m
Emil Vikstr&#246;m

Reputation: 91942

Common Lisp does not let you redefine if. You can, however, invent your own control structure as a macro in Lisp and use that instead.

Upvotes: 2

Related Questions