Functional Programming: How to handle complex data without bloated functions?

Lets say in your program you have defined a complex car object. That object holds a very long list of predefined key value pairs (wheels,engine,color, lights, amountDoors etc.), each being either a part number or a list of part number, or a specific value.

//** PSEUDO CODE:
var inputCar = { 
  "engine": "engine-123", 
  "lights": ["light-type-a", "light-type-b"], 
  "amountDoors": 6,
  etc ... lets assume a lot more properties
}

Lets also assume, this object is already as simple as possible and can not be further reduced.

Additionally we have a list of settings, that tells us more information about the part numbers and is different for each kind of part. For the engine it could look like this:

var settingsEngine = [
  { "id": "engine-123", weight: 400, price: 11000, numberScrews: 120, etc ... },
  { "id": "engine-124" etc ... }
]

With all the settings being bundled in a main settings object

settings = { settingsEngine, settingsWheel, settingsLight ... }

Now we have different functions that are supposed to take a Car and return certain values about it, like weight, price or number of screws.

To calculate those values its necessary to match the IDs from the input car, with those from the settings, and also apply some logic to get the exact data of complex parts (to figure out what the autobody looks like, we need to see how many doors there are, how big the wheels are etc.).

Getting the price would also be different and arbitrarily complex for each part of the car. Each part of the pricing could need to access different parts and information about the car, so just mapping over a parts list wouldn't suffice. (For the price of the paint job we would need the total surface area of all parts with the same color etc.)

One idea would be to create an inbetween object, that has resolved all the details about the car that are shared between the price and weight calculations and can then be used to calculate the weight, price etc.

One implementation could look like that:

var detailedCar = getDetailedCar(inputCar, settings);

var priceCar = getPriceCar(detailedCar);
var weightCar = getWeightCar(detailedCar);

This way part of the work has only to be done once. But in this example detailedCar would be an even more complex object than the initial input object, and therefor so would be the parameter of getPriceCar - making it also really hard to test, because we would always need a full car object for each test case. So I am not sure if that is a good approach.

Question

What is a good design pattern for a program that handles complex input data that can't be further simplified in a functional programming style/with pure functions/composition?

How can the the result be easily unit-testable given a complex, interdependent input?

Upvotes: 3

Views: 736

Answers (2)

Zazaeil
Zazaeil

Reputation: 4119

I would suggest slightly different approach here.

Since your question is about purely functional programming, I would say you need a higher order function responsible for lightening needed bits of complex datastructure and shadowing unncessary ones: readComplexDataStructure :: (ComplexDataStructure -> a) -> (a -> b) -> ComplexDataStructure -> b, where a represents the data you need to extract from some ComplexDataStructure instance and b is a result of a computation.

Please note how close is it to the Reader monad, though I wouldn't recomend to use it rightaway unless code complexity justifies such a decision.

P.S. It scales. You just need a function to produce n-uple made of (ComplexDataStructure -> a) projections. As an example, consider following signature: double :: (ComplextDataStructure -> a) -> (ComplexDataStructure -> b) -> ( (a, b) -> c) -> ComplexDataStructure -> c. Your code wouldn't become "bloated" as long as you maintain appropriate projections only, all the rest is quite compoistional and self-descriptive.

Upvotes: 2

Bob Dalgleish
Bob Dalgleish

Reputation: 8227

The general term for what you describe is in the use of projections. A projection is a data structure that is an abstraction of other data structures, oriented towards the kinds of calculations you want to make.

From your example, you want a "screw projection", which takes the data that describes a vehicle and comes up with the screws that are required. Hence, we define a function:

screwProjection(vehicle, settings) -> [(screwType, screwCount)]

which takes a vehicle and the settings that describe components and comes up with the screws that make up the vehicle. You can also have a further projection that simply sums the second item in the tuple if you don't care about screwType.

Now, to decompose screwProjection(), you will need something that iterates over each component of the vehicle, and breaks it down further as needed. For instance, the first step in your example, get the engine and find the settings appropriate to engines, and filter based on the engine type, then filter that result based on the field for screws:

partProjection(part, settings) -> [(partType, partCount)]

So, screwProjection() looks like:

vehicle.parts
  .flatMap( part -> partProjection( part, settings ) ) // note 1
  .filter( (partType, partCount) -> partType == 'screw' )
  .map( (partType, partCount) -> partCount )
  .sum()

Note 1) This projection method does not allow for nested bill-of-material lookups, which you may want to add for extra credit.

This general approach of enumeration => projection => filter => reduce is at the heart of many functional programming paradigms.

Upvotes: 4

Related Questions