Thinking Functionally. Building a New Array in Has

2019-06-21 19:34发布

问题:

I'm new to functional programming, and I've decided to build an app in Purescript. I've hit my first hurdle, and I'm not sure how to think about this conceptually.

I'm not looking for code as much as a way to think functionally about this problem.

I have a list of data. Specifically, something like

[ {a :: String, b :: String, c :: String} ]

I would like to create a list of Html (which is a purescript-halogen type) by using the record provided (with a list of the above types).

So, I would have a function

buildElements :: forall p i. MyRecordObject -> Array (HTML p i)

Now, I think I'm going to need to give this function result type a Monad computational context (purescript Eff is like Haskell IO)

So something like:

buildElements :: forall p i. MyRecordObject -> Eff (Array (HTML p i))

My first idea was vaguely around creating a list with something like

take $ length xs $ repeat ARecordObject

and then map the record over that list, but I wasn't really sure how to translate that into code. It seemed wrong anyway, since my plan involved mutating the state of ARecordObject, which is a no-no.

So then I found this function:

forEach :: forall e a. Array a -> (a -> Eff e Unit) -> Eff Unit

which looks almost perfect! I get an array, I give it a function that somehow assigns the properties in the record to this new array...but no, wait...I'm thinking non-functionally again.

I'm really at a bit of a loss here. Basically, I want to create something like a list of <li></li> elements, where I assign properties to each item.

E.g

I'm provided a record with:

[ { id: "id1", name: "name1", class: "class1", content: "content1" }
, { id: "id2", name: "name2", class: "class2", content: "content2" } ]

And I would like a function foo that returns an array:

[ li [ id_ rec.id, name_ rec.name, class_ rec.class ] [ text rec.content ]
, li [ id_ rec.id, name_ rec.name, class_ rec.lass ] [ text rec.content ] ]

where rec is the name of the recordObject (and obviously the two arrays are not identical, but actually mapped over the initial record).

(the dot syntax is a purescript record syntax notation similar to standard getter/setter notation)

回答1:

My first idea was vaguely around creating a list with something like

take $ length xs $ repeat ARecordObject

and then map the record over that list, but I wasn't really sure how to translate that into code. It seemed wrong anyway, since my plan involved mutating the state of ARecordObject, which is a no-no.

Functional programmers don't just avoid mutation because it's a no-no (indeed, many functional programs make careful use of a controlled dose of mutability) - we do it because it produces safer, simpler code.

To wit: You're thinking in what I call "alloc-init mode", wherein you create some sort of "empty" value and then go about calculating its properties. Forgive my vehemency, but that's a fundamentally broken programming model, left over from the days of manual memory management; code which uses it will never be safe and abstractions relying on it will forever be leaky. The idiom doesn't fit into any language that's higher-level than C, and yet, if I had a pound for every time I see code like this...

var foo = new Foo();
foo.Bar = new Bar();
foo.Bar.Baz = new Baz();

...I would be a rich man (na na na). The default should be to create objects after you know what they're going to look like:

var foo = new Foo(new Bar(new Baz()));

This is simpler - you're just calculating a value, rather than reaching into the memory referenced by a pointer to update its contents - and more importantly it's safer because the type-checker ensures that you haven't forgotten a property and it allows you to make Foo immutable. The cleanest imperative code is functional code - you should only be imperative where necessary for performance (or when the language forces your hand).


Anyway, rant over. The point is that you're making life harder for yourself than necessary by thinking imperatively. Just write a function that calculates a single <li> from a single object...

toLi :: MyRecord -> HTML
toLi x = li [ id_ x.id, name_ x.name, class_ x.class ] [ text x.content ]

... (note that I'm not somehow creating an "empty" li and then populating its values), and then map it over your input list.

toLis :: [MyRecord] -> [HTML]
toLis = map toLi

This is how I'd do it in JS, too, even though I'm not required to by the language. No side-effects, no mutation, no need for Eff - just simple, safe, purely functional code.