3

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?

  • 1
    I'm not sure it has anything to do with your program being functional or not. Your question is mostly about typing, and different languages will have different mechanisms in order to deal with such problems. For instance, an object-oriented language has inheritance and polymorphism, whereas a modular language uses inclusion and subtyping. – Richard-Degenne Feb 25 '19 at 11:11
  • But inhertiance and polymorphism wouldn't be an optione - thats why the question asks, how to solve that in a functional way. How is this problem best approached, without ending up with a function that takes either a giant list of parameters, nor very complex objects. — Which patterns can be used to break up such complex data structures? – Marcus Blättermann Feb 25 '19 at 20:51
  • 1
    @MarcusBlättermann inheritance and polymorphism are not non-functional. A polymorphic function can easily be a pure function. – Bergi Feb 25 '19 at 21:35
  • Functional means (more or less) "without side effects". You can write classes where none of the methods produce side effects and write object-oriented functional code. Your example being a read-only problem (you're not trying to create new cars or to change existing cars), this would be very easy. Write constructors, getters and use a Factory pattern for the settings and you're good to go. – Richard-Degenne Feb 26 '19 at 16:04

2 Answers2

4

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.

Bob Dalgleish
  • 8,167
  • 4
  • 32
  • 42
  • Could someone provide any links/resources to learn more about this concept of Projections? My search engine pretty much only returns this answer when I look up variations of "functional programming projections" – Samuel Ludwig Aug 11 '21 at 19:22
  • It's not really a functional programming thing. If you used a relational database, you would create a SQL (structured query language) statement that did the relevant joins and select only those fields that you need. – Bob Dalgleish Aug 17 '21 at 14:50
2

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.

Zazaeil
  • 3,900
  • 2
  • 14
  • 31
  • Uh, judging from the signature, your `readComplexDataStructure` is just function composition? – Bergi Feb 25 '19 at 22:06
  • @Bergi, at the end of the day, almost everything in functional programming - any more or less sophisticated idea - is nothing but a function compisition. In my case it is indeed and one might decide to bypass named instacne and prefet raw `(.)` compistion instead. Though the concept behind won't change: you project to a more spefic case and then feed it into appropriate consumer. – Zazaeil Feb 25 '19 at 23:17