Going Declarative with Functional programming

The Partridge Family were neither partridges nor a family. Discuss.
albinopapa
Posts: 4373
Joined: February 28th, 2013, 3:23 am
Location: Oklahoma, United States

Re: Going Declarative with Functional programming

Post by albinopapa » March 16th, 2019, 4:30 am

TL;DR Declarative programming from what I can surmise from the sites I've visited describe a way of abstracting away the implementation details, but the details are still there. It avoids as much as possible the use of loops and conditional blocks, replacing loops with recursion, not sure of alternatives for conditional blocks. I stand behind my claim that functions are a list of commands, whether they be a single function call that sums an array of numbers or the very function that just sums all the numbers in a loop. Library developers will need to use imperative programming to make their library more declarative programming friendly.

I HAVE NO IDEA WHY I'M SO HUNG UP ON THIS IDEAL.

I think the reason I'm so hung up on this is the absurdity some of the claims. If you need fast single threaded programs and/or a small memory footprint, you use imperative programming. If you are writing a library, you of course what to make it as easy as possible for others to use, so make the interface in a way that supports declarative programming ideals such as return by value and abstracting away the hundreds of steps it might take to accomplish some task, but you will probably be implementing that library using imperative programming idioms with state, loops and conditional statements for logic and flow control.

Had some time to kill so I thought I'd look around at some other sites to help understand declarative programming a bit deeper, but unfortunately I have a huge issue with one of the main focal points. The common theme seems to be to remove loops and condition blocks and how much the emphasis is on making the user facing code more expressive.

The user gets to call: Sandwich sandwich = MakePBJ(bread, peanut_butter, jelly);
But it says nothing about the implementation of MakePBJ(). ( I UNDERSTAND THAT FROM THE USER'S STANDPOINT they don't need to know how MakePBJ works ). I mean, the steps to make the sandwich have to be written by someone. So, there might be a:

Code: Select all

if( jelly.IsStrawberry() )
   return peanut_butter_strawberry_jelly_sandwich;
else if( jelly.IsGrape() )
   return peanut_butter_grape_jelly_sandwich;

//or

switch( jelly.GetType() )
{
   case Jelly::Type::Strawberry:
      return peanut_butter_strawberry_jelly_sandwich;
   case Jelly::Type::Grape:
      return peanut_butter_grape_jelly_sandwich;
   default:
      return peanut_butter_no_jelly_sandwich;
};
Ok, so ways around this is to use function overloading, inheritance or std::variant I get that, but somewhere I'm almost guaranteeing that there is a need for imperative programming. As a matter of fact, one site said that functional programming fits best when functions don't require a specific order for the CORRECT result ( like graphics pipelines or image/video/audio processing as I mentioned earlier ).

So, what I said about functions just being a collection of commands still holds true in my eyes, regardless how you "pretty-ify" them.

WHAT TO TAKE AWAY, IN MY OPINION
The idea of declarative programming is to abstract the steps taken to complete a task and present a simpler more expressive interface to end users. The library developers would use imperative programming where all the control flow and other logic is, the users of those libraries get an easier to understand interface without having to know all the implementation details.

Parsing commands from a string requires a lot of conditions and you can argue all you want, but looping. Even if it's recursion, you still have to iterate through the entire string and jump to certain cases depending on input.

I'm slowly making my way to making an interpreter and there are a few places that I have been able to clean up how the code looks. One place is removing comments, both single and multiline. I simply call:

Code: Select all

const auto noSingleLineCommentsSource = RemoveSingleLineComments( source ); 
const auto noComments = RemoveMultiLineComments( noSingleLineCommentsSource );
const auto separatedSymbols = SeparateSymbols( noComments );
const auto tokenStrings = TokenizeString( separatedSymbols );
const std::vector<std::string, Token> tokens = ConvertTokenStrings( tokenStrings );
I still haven't gotten to an AST yet, and this is probably going to take me a while to work out how to create one...I hate recursion, but I have to get over it and try to understand it in order to finish this project.
Back to the story, While this looks simple and straight forward, the implementation of those functions are riddled with loops and if/else if or switch/case statements.

Just to parse a floating point number for instance, there are a few things to consider.

Code: Select all

// Acceptable
3.0f
3.0F
3.f
3.F
.05f
.05F
0.05f
0.05F

So, as I'm iterating over the characters, I run across a '.' and have to decide: was the previous character a space?  Was it an alpha character ( a-z, A-Z )?  Was it a digit ( 0-9 )?

So begins the condition:
// Most likely a double or float
if( isspace( source[i-1] || isdigit( source[i-1] ) )
{
   // Read all characters until next space or 'f' or 'F'
   int j = i;
   while( isdigit( source[j] ) || ( toupper( source[j] ) != 'F' ) && ( j < source.size() )
   {
      // Ignore all characters that may be invalid, will check later
      ++j;
   }

   // Assumed to be a float, add 1 to j to include the 'f'
   if( toupper( source[j] ) == 'F' )
   {
      ++j;
   }

   // Append next step's string with the range of characters belonging 
   // to the floating point number
   for(int idx = i; idx < j; ++idx )
   {
      lexed.push_back( source[ idx ]; );
   }
}
// If source[i-1] is letter or '_', then most likely '.' is used as a member accessor
else if( isalpha( source[i-1] ) || source[i-1] == '_' )
{
   // Just add characters to next steps string until next separator
   while( isalpha( source[i+1] ) || source[i+1] == '_' && !IsSeparator( source[i+1] ) )
   {
      const auto j = i+1;
      ++i;
      lexed.push_back( source[j] );
   }
}
Not sure how declarative programming can help out here. Yes, I can abstract the code within the condition blocks to hide away the steps and return a string that if NOT empty would be appended to the lexed string, but the if statements would still be there for logic control.

In my opinion, loops are easier to understand than recursion, it's one reason I find template metaprogramming so difficult.
Conditions are a part of life, you can't get around them unless your code is linear and predictable in which case, it might fit a certain domain, but not all.

You made the comment about me learning what you meant when I do GPGPU programming, and I've meddled around with C++AMP, which uses the C++ language, but is still run on the GPU. I've written basic HLSL shaders and they accept condition blocks like if/else and for loops. I've never written a compute shader ( DirectCompute ), but I know that you pretty much use the return parameter to get a result from functions as there is no pointers or references. Even in C++AMP within the class members decorated as (restrict amp) don't capture the 'this' pointer, which makes some things difficult.

To make a better comparison about computers being just really fast calculators, I'd say that is partially untrue. GPUs while have limited branching capabilities, are more like super fast calculators than CPUs as they have a lot more "working parts". They don't just do calculations. There is branching, there is just simple data movement. Programs can be more complex than just crunching a database of numbers.

The GPU is designed to do just that, crunch a bunch of numbers and transform that data into data that can be displayed as graphics or produce audio effects. As long as it all happens in the same frame it doesn't matter if pixel[0] is processed first or pixel[ 1'000'000 ] is, the frame isn't displayed until it's all finished. As long as the current sound buffer is filled before presentation, it doesn't matter what sample gets reverb added first or fifth.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com

Post Reply