Feed Icon RSS 1.0 XML Feed available

Pseudo-functional programming tricks in C++

Date: 15-Mar-2005/19:10

Tags:

Characters: (none)

Wouldn't you be upset if you discovered that sqrt(9) was 3 on weekends and 3.7 every other day of the week? I certainly would.
Yet in languages like C++, there is nothing fundamental about the language which keeps a misguided library author from writing things like:
float sqrt(float x)
{
   if (x == 9)
      if (today() == Saturday || today() == Sunday))
         return 3;
      else
         return 3.7;
         
   /* ... */
}
Of course, you'd hope that the authors of the standard math libraries wouldn't do something so blatantly crazy. But you can't really protect a module from a dependency you don't want to arise in C++. So any function in any library can--theoretically--examine any information it wants. That information might live in a global variable, somewhere in the file system, in the video buffer, or even on the Internet.
To address this issue, computer science academics often advocate ditching traditional OOP entirely and moving to functional programming. This lets you contractually define operations which are guaranteed to depend solely on the parameters you give to them. This is great, but it requires a certain "stylized" way of thinking...and it doesn't trust programmers to "do the right thing". So functional programming often makes seemingly-simple procedures a hassle to write.
Yet it's possible to abuse C++ a little, in order to be relatively sure certain methods aren't depending on certain things. For instance, let's imagine that you have a Region object type which is going to receive some number of Enter() and Exit() calls, followed by a call to RunAction():
class Region
{
   /* ... */

   virtual void Enter() = 0;
   virtual void Exit() = 0;

   // supply action code here, by overriding "RunAction"
   // please do not make behavior depend on how
   // many times Exit or Enter was called, only
   // depend on what the last call was!
   virtual void RunAction() = 0;
   
   /* ... */
};
Notice that you would very much like RunAction()'s effects to NOT depend on how many Enter()s or Exit()s happened. Yet the comments do little to enforce this: if any of Region's members are modified by the Enter() or Exit() calls, that might introduce a dependency! If you want to discourage the RunAction() method from depending on those modifications to Region, you can introduce a cooperating class:
class Action
{
   /* ... */

   virtual void Run(bool inside) = 0;
   
   /* ... */
};

class Region
{
   /* ... */

   virtual void Enter() = 0;
   virtual void Exit() = 0;
   virtual std::auto_ptr<Action> AllocateAction() = 0 const;
   
   /* ... */
};
I've used a smart pointer here (std::auto_ptr), to indicate a transfer of ownership. The Action that Region::GetAction returns is owned by the caller and can be freed by it. That's important!
Before any Enter() or Exit() calls are made, the caller asks the Region object to construct an Action object which predetermines the final action. The Action object does not have access to the state which may be accumulated by the Region object, and vice-versa. Yet Region still drives the logic--it was able to give us a class which dictates what will happen when a Action::Run() is finally invoked...but without being able to make that execution conditional on the state it might gather.
On the surface this looks good, but it can be undermined (unintentionally or otherwise) in several ways. For instance, the Region object could poke a this pointer into the Action it returns, thus giving the Action access to its state to the later call to Action::Run(). However, as long as we require (by convention) that Region::AllocateAction() produce an isomorphic object on each invocation, the caller can:
  • Construct an object region1 of type Region
  • Call region1::AllocateAction() to produce an object action of type Action
  • Construct a new object region2 of type Region (same constructor as step 1)
  • make the Enter() and Exit() calls on region2
  • optional: make a few spurious Enter() and Exit() calls on region1
  • Destroy region1
  • Call action::Run(inside) where inside indicates whether we are inside region2
This obviously hampers the ability of Region developers to build a coupling between the Region object and the Action object it hands you. It's not exactly functional programming, and of course a programmer could subvert any hard-coded strategy you might come up with (it's even better if you do something more random). Yet it should catch most major violations of the model, and alert a programmer who is simply unaware of the dependency that they were doing something wrong.
Business Card from SXSW
Copyright (c) 2007-2018 hostilefork.com

Project names and graphic designs are All Rights Reserved, unless otherwise noted. Software codebases are governed by licenses included in their distributions. Posts on blog.hostilefork.com are licensed under the Creative Commons BY-NC-SA 4.0 license, and may be excerpted or adapted under the terms of that license for noncommercial purposes.