Algebraic Structures: Things I wish someone had explained about FP
jrsinclair.com
jrsinclair.com
interface Functor<A> {
map<B>(f: (a: A) => B): Functor<B>;
}
But the TypeScript definition loses something important: the point of a Functor is that you get back the same data type -- there's one `f` in the Haskell defn both in the argument and return type, while this TS definition can give you back any random data type. E.g. the implementation of Array.map could give you back an Either.I don't mean this comment to be a random nitpick of the post. Trying to think these things through is hard, and trying to use things you know to help understand the new idea is not unreasonable. But in particular with PL each thing has so much associated baggage (here in TS, subtyping) that reasoning "by metaphor" often means you end up missing the critical point.
[1] https://github.com/gcanti/fp-ts [2] https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-ki...
interface Functor<A> {
map<B>(f: (a: A) => A): Functor<A>;
}
You map over an option, you get an option. You map over an either, you get an either. etc etc.So you end up with
interface Functor<A> {
map<B>(f: (a: A) => A): Self<B>
}
where `Self` needs to recognize that the type being defined to implement this interface has a "slot". This tends to make things tough.I'm a very low level functional programmer. I'm big on immutability, big on not using loops and instead using map/flatMap/filter/fold, I tend to roll my own either and option implementations when they don't exist because it's the tidiest way of handling errors I've come across, etc etc. But when it comes to stuff like functors I don't get what it's buying me. What interesting stuff can I do if I know that both an option and a list can be mapped over?
I really need to look more deeply into it at some stage. I might be missing out on some powerful tools. Or it might be a bunch of stuff that's theoretically interesting but practically useless.
This gets handle by the Cats library in Scala (https://typelevel.org/cats/typeclasses/functor.html) by defining the type class the functor is being defined for as an abstract type on the functor itself.
It's very important for the type of `fmap f x` to be identical to `x` except in that its "inner" type has been modified. Without that, these kinds of interfaces lose all of their value.
The interface only requires you to implement the mapping between morphisms, but a mathematical functor also includes mappings between the objects from one category to the next.
Additionally fmap is actually mapping
(a -> b) to (f a -> f b)
but the way it's typically used in haskell breaks this intuition. Most people see it as ((a -> b), f a) to (f b)
as a result of how it's used in FP. Technically the usages are isomorphic but using it this way blocks your intuition from fully understanding the true nature of a functor.The following would be more a more accurate type class for a functor:
class Functor f where
fmap :: (a -> b) -> (f a -> f b)
tmap :: a -> f a
See here:https://en.wikipedia.org/wiki/Functor
Note that there are TWO axioms for functor, I added the second axiom to complete the definition.
I think the reason why tmap doesn't exist is because it's trivial? Not sure maybe some expert can pitch in as I'm certainly not an expert in haskell.
The typeclass you created would be a functor along with a natural transformation from the identity functor to f. I don't think these always exist, but they do for applicative functors (pure/return).
The object mapping is reflected at the type level. "f a" is the object that "f" sends "a" to.
In Haskell, functors are all endofunctors. In math, functors can be between categories, and in such a case a -> f a might not make any sense because there are no morphisms between categories.
Using some quasi-Agda syntax, here would be a full definition of a functor between two categories. Maybe you could overload "->" so that "X -> Y" could mean "Mor X Y" for the set of morphisms between X and Y in C or D (depending on context). Curly brackets mean optional parameters, which Haskell approximates via typeclasses.
record Functor where
field
C, D : Cat
F : Ob C -> Ob D
fmap : {X Y : Ob C} -> Mor{C} X Y -> Mor{D} (F X) (F Y)
fmapId : {X : Ob C} -> fmap id == id
fmapComp : {X Y Z : Ob C} -> {f : Mor X Y} -> {g : Mor Y Z}
-> fmap g . fmap f == fmap (g . f) Const q a = Const q
fmap _ (Const q) = Const q
is a perfectly viable Functor on a. Good luck converting a a into one of those.The natural transformation would be a function "a -> Const q a" for all a. There can't be a way to do this for all q that is somehow natural in q, since this would have to construct a value of q to put into the Const constructor. Worse, (even though vanilla Haskell doesn't allow it), q might be the empty type, so there might be no functions "a -> Const q a" whatsoever. I think the point of the example is that keeping q polymorphic is a simulation of the empty type.
Allowing empty types, I'm happy with the example. However, this is technically a natural transformation in Haskell:
f :: a -> Const q a
f x = Const undefined x
since every type contains bottom (undefined). data Nullity a
This is a perfectly valid functor, too. Ignoring undefined, there are no values of it, but fmap cannot be used to create values of the mapped type, just manipulate them.In CAT the category of small categories objects are categories and morphisms are functors. Haskell types can be thought of as a small categories so I don't think this statement is correct? I'm no expert but can you clarify, the definition on wikipedia says that functors are morphisms between categories in CAT.
>There might not be a corresponding morphism between "a" and "f a".
Hmm. if haskell types make up a small category that means 'a' and 'f a' are sets. I can't imagine a case where two sets cannot have a mapping between them. Can you give a specific example?
>The typeclass you created would be a functor along with a natural transformation from the identity functor to f.
I'm confused about this. Isn't the natural transformation from identity to another category (at least for small categories) equivalent to a functor? In my head they look identical.
https://en.wikipedia.org/wiki/Natural_transformation
If you look at the commutative diagram on this page and replace F(X) and G(X) with identity X and G the natural transformation diagram looks just like a diagram for a functor.
btw I'm not an expert in haskell (let alone agda) or category theory so forgive me if the stuff I talk about is totally off.
This is irrelevant. The question isn't, does there exist a function, it's does the functor determine one. (Given any sets A and B there is always a function from A to B unless B is empty and A is nonempty. But that's not very helpful, is it?)
> I'm confused about this. Isn't the natural transformation from identity to another category (at least for small categories) equivalent to a functor?
This doesn't mean anything. A natural transformation is from one functor F:C->D to another functor G:C->D. Not from a functor to... a category? Huh?
Also smallness is irrelevant to all of this.
Can you clarify this? I can't define a -> f a because the functor can't determine one?
What about like for list functor?
Int -> [Int]
Doesn't this work?>This doesn't mean anything. A natural transformation is from one functor F:C->D to another functor G:C->D. Not from a functor to... a category? Huh?
I'm still kind of confused. Yeah you're right it doesn't make sense. But then the parent poster is saying that
a -> f a
is the natural transformation from identity functor to f it seems off to me. It looks to me like it's just a functor from a to f a. So I'm confused with the nomenclature here. a is a category and f a is another category how is a mapping between categories a natural transformation?Isn't this type signature the natural transformation from identity to f?:
(a -> a) -> f a
--edit:I think I see where I'm confused. Your last comment on the parent helped. Thanks.
Sorry I confused you here. I meant, if you had a function of that type (a -> f a), you would want a naturality relation to hold for it, like how 'return' for monads is meant to relate to fmap.
As it is, just having a map "a -> f a" is not a natural transformation.
(I think you might be confusing objects of Hask with the category Hask itself. Remember that "a -> f a" means "for each object 'a' of Hask, a function that takes an element of 'a' to an element of 'f a'. The functor is f : Hask -> Hask, an endofunctor. (In haskell, Hask is replaced by stars.) "f a" is the object of Hask that F sends "a" to.)
In mathematics, it's important to distinguish between "an X such that there exists a Y" and "an X together with a Y".
We are discussing, what data determines a functor? You seem to be asserting that part of this is something you're calling tmap, which for a specific functor F would have type a -> F a.
This is mistaken; no such thing is part of the definition of a functor.
But you are confusing the issue of whether such a map is part of the definition of a functor (or is determined by the functor and so could be included in the definition with no change in the actual meaning; let's just group these two cases together as "part of the functor"), with the issue of whether such a map exists.
You pointed out that such a map exists -- as if that made it part of the functor. I pointed out that such a map does exist, but that's not helpful, because this has no relation to the functor, and that it's not part of the functor.
Your reply confuses these issues again, suggesting that because it's not part of the functor like I said (true), therefore it doesn't exist (false). No. It does exist, but it's not part of the functor.
Because it's not part of the functor -- not determined by the functor -- that means that, if you were to tack it on to the definition of the functor -- let's call this new object you're defining a functor' -- then, for any one functor, there would be multiple possible functor' that result, because of the different choices that can be made. This demonstrates that a functor and a functor' are not in fact the same thing; the functor' contains extra information that the functor does not, information that cannot be determined from the functor alone.
> Isn't this type signature the natural transformation from identity to f?: > (a -> a) -> f a
No. You are mixing things in ways that do not make sense.
Look, apologies, but I think you're in over your head here. And I'm afraid your notation is confusing the issue by mixing things inappropriately. There are too many errors here to fix individually; if you want to get this right, you're going to have to take it from the top. I could try to write an explanation that I'd hope you might understand, but my point is that that's what I'd have to do, write a whole explanation from the top. I don't see this conversation going anywhere otherwise.
I'm out of responses here on HN so I'll respond tomorrow.
I'm not a mathematician, so yeah I am a bit over my head.
It is true that functors are morphisms between categories, but that's a morphism in CAT.
> I'm confused about this.
That's how these things go :-) Anyway, natural transformations are between two functors C -> D. This is an example of a 2-morphism in a 2-category. It's hard to come up with something to say other than they're just different, but think about this: a natural transformation is a consistent choice of morphisms F X -> G X (in D) for each object X (in C), but a functor needs to know where the morphisms of C go, too.
(An intuition is a natural transformations is kind of like a continuous path transforming one functor into another. This might just make things confusing, though.)
This is incorrect in a bunch of ways. First, Haskell types aren't truly just sets. As a simple, practical example you might have
module Container (makeInt, Container, getValue) where
data Container a
= Container { getValue :: a }
deriving Functor
makeInt :: Int -> Container
makeInt = Container
And now we've got (externally) a type which is equivalent to the identity functor in structure, but has a weird interface which disallows tmap.More powerfully, we're only actually interested in discussing an interface in and of itself. A type may instantiate many interfaces of varying levels of power, but each interface needs to be well-defined and well-behaved on its own. Then their compositions need to be "glued together" properly.
So even if all Haskell Functors were actually TMapFunctors, it's still important to note that the interface TMapFunctor is a sub interface to Functor which allows more operations and has more laws.
An even stronger example of this is the fact that due to the way Haskell arrows work, all Haskell Functors are actually "strong" functors in the sense that we cannot even truly specify a non-string functor.
Strength is a way that fmap and products (or, really, category tensors) interact.
strength :: Functor f => (a, f b) -> f (a, b)
strength (a, fb) = fmap (\b -> (a, b)) fb
This seems completely obviously possible, but it's only because we can crack open products in building general anonymous arrows. That's not supported in every category and a generic functor should not be expected to satisfy it.Can you explain this? What is a strong functor and how does it have to do with arrows? Also what do you mean by non-string functor?
And “non-string” was just a typo.
The answer is it does. In this context, the types are the objects. So if F is your functor, then F itself -- which does not have a type, but has kind * -> * -- is the mapping on the objects.
I think I get it. Thanks.
The notation "a -> f a" means a function that sends elements of 'a' to elements of 'f a', where 'a' is an object of the category Hask. (Functors don't look into the objects at the element level.)
Caveat: there seems to be some reason not to think of kinds as actually being the types of datatypes, but I don't understand why that is.
As for the quoted definition, I agree that if this is your first exposure to abstract algebra, you'll need a moment to unpack the sentence, follow some links, and especially, read on. But that's not just Wikipedia; the blog post also takes considerably more than one sentence to explain what it is trying to say. You can't explain everything in one sentence.
For whatever it's worth, as a data point relating to a recent discussion on whether a university CS education makes you a better programmer or not: We literally started learning about algebraic structures in the first math class on the first morning of the first year of university. If you're going to program in a setting where algebraic structures (or data types!) are relevant, this university knowledge will help.
Finally, this blog looks very nice visually, but the "broken typewriter" font effect makes the code examples much too hard to read. It would be great if the ribbon in that typewriter could be replaced.
Does this really happen? Do people really say generic "algebra" for any set with operations?
(And as an aside, I don't like the "abstract algebra" monicker. It sounds so immature and undergraddy. There isn't an ordinary algebra and an abstract algebra. It's all just algebra.)
Most of the structures called algebras (e.g. boolean algebras) have at least two operations and these operations frequently interact via distributivity or something like that. Other examples are ring-like, like a sigma algebra in measure theory.
I've never really encountered anyone saying "algebra" for a generic algebraic structure. I think with this definition Cohn was trying to start a trend that didn't catch on.
Which one(s) you have been exposed to just depends on which mathematical subcultures you’ve interacted with.
Mathematics Wikipedia is sometimes very biased towards certain points of view (usually more undergraddy, as evidenced by its insistence with the "abstract algebra" term), so I think it's being a bit too pushy with its "algebra" definition here. I've edited the term to say that this usage of "algebra" is a universal algebra thing.
If I have a structure S with an associative operation, and another structure G with an associative operation and a neutral element, I will say that S and G are different algebras, not "different magmas". Others looking at S or G will not ask "oh, what kind of magma do you have there", they will ask what kind of algebra.
So... Yes, these are both (special cases of) magmas, but the general term used for them is "algebra" or "algebraic structure". Don't you agree?
So you're saying people shorten the phrase "algebraic structure" to "algebra"; this hasn't been my experience.
Of course I added extra structure since I wanted to make a point about different kinds of algebraic structures which are all subsumed by the term "algebraic structure" or "algebra". And it's only possible to distinguish kinds of algebraic structures by differences in structure.
But adding extra structure in one example doesn't mean that I somehow exclude magmas from the definition. Here is the example again, extended to be include a component with no extra structure:
If I have a structure M with no structure but an operation, a structure S with an associative operation, and another structure G with an associative operation and a neutral element, I will say that M and S and G are different algebras, not "different magmas". Others looking at M or S or G will not ask "oh, what kind of magma do you have there", they will ask what kind of algebra.
Of course this extension by M doesn't change anything about the validity of the example. Magmas are just as included in the term "algebraic structure" as semigroups, groups, rings, and fields are.
> So you're saying people shorten the phrase "algebraic structure" to "algebra"; this hasn't been my experience.
<shrug> It has been mine. Wikipedia has lots of uses of the phrase "the algebra of": https://en.wikipedia.org/w/index.php?search=%22the+algebra+o..., always meaning something like "the algebraic structure of set X with operations f, g, and h".
Algebraic stucture, sure, but _algebra_, absolutely not.
An algebra is a module with a compatible multiplication which has an identity element. If I had a magma and you asked about my "algebra" I would be very confused about where you were seeing all the extra structure.
As someone else pointed out elsethread, the term seems to be overloaded in different branches of mathematics.
https://en.wikipedia.org/wiki/Universal_algebra: "Universal algebra (sometimes called general algebra) is the field of mathematics that studies algebraic structures themselves, not examples ("models") of algebraic structures. [...] In universal algebra, an algebra (or algebraic structure) is a set A together with a collection of operations on A."
I think this comes from competing definitions of the term 'algebra'. There's what, for want of anything better than the terrible term, I'll call the 'algebraist's algebra', which is (at least) a ring that is compatibly a module over some other ring; and there's what, as smadge (https://news.ycombinator.com/item?id=21443587) mentions, could be called the 'universal algebraist's algebra', which is a model for a certain signature, of which the algebraist's algebra is just one special case. EDIT: I see that matt_noonan (https://news.ycombinator.com/item?id=21443892) made this point several hours ago, but I leave this post in case the links below are convincing.
For sources maybe more authoritative than Wikipedia, you might consult Springer's Encyclopedia of Mathematics (https://www.encyclopediaofmath.org/index.php/Variety_of_univ...) or the nLab (https://ncatlab.org/nlab/show/universal+algebra).
But that is different from an algebraic structure. An algebraic structure is just some set with some operations and laws. It can be magmas, monoids, groups etc.
And abstract algebra is also a necessary term to differentiate from "ordinary" algebra that is taught in middle schools and high schools, where algebra only means using letters to substitute numbers.
It's unfortunate.
My problem with this argument is that this isn't a universal experience. I went to Waterloo, arguably one of the best CS schools in Canada, and the CS program didn't even cover algebraic structures.
I learned about them, because I spent all my electives taking extra math classes, but the vast majority of my classmates never needed to learn any math beyond basic combinatorics and some introductory complexity analysis.
I believe that these topics are incredibly important, but I'm not convinced that a college degree is a reliable way to be exposed to them. If your school covers this stuff, great! But at least in my experience, I think it's disingenuous to act like all computer science programs will.
I think functional programming uses category theory in a different fashion than even fairly advanced mathematics; Breaking Hungerford's Algebra text, in the chapter on Category Theory, he writes, "A significant part of the elementary theory of categories is the attempt to generalize as many concepts as possible from well-known categories (for example, sets or modules) to arbitrary categories". Which is a rather different approach than building programs.
Which is to say that for most mathematics, categories are tools for generalization or for providing a firmer foundation for existing mathematical structures. For understands monads as used by FP, the description as "little languages" seemed the best - it's way of not having side effects by using functions to incrementally construct output instead of doing output in the middle of computation.
If I dream up a way to tackle a problem, have I made a category? If I have two modules, can I compose them into one, is there an initial or final module, is composition associative? If there's some underlying structure, what's the free module based on it?
Those questions can help make more durable designs.
Also, we did functional, logic and also imperative programming in the first year. I really did live under the impression that CS education is meant to give you all these foundations, and it's a TIL today that it's not universally true.
The advantage really plays out more with the first-order structures, too. Things like monoid, semiring, torsor, group. You also have nice ones in more standard data structures: a balanced tree is an excellent example of a structure where the laws exist to cut out unbalanced trees.
In my opinion, there are two things to study here:
First, the practice of thinking about abstract structures that apply to concrete data. For this, the practice of thinking of there being a type (or multiple interrelated ones) which offers some set of "constructors" which create the type or augment existing values (gluing new items into a tree, merging two trees, etc) and some set of "laws" for which all values of that type must uphold.
It turns out that you can do a lot of analysis of the behavior of these structures in the abstract and then apply it wholesale throughout programs. Many concrete values you work with are the combination of multiple structures in natural ways. Sometimes you can replace whole APIs with hundreds of calls, each named uniquely to this implementation of this type, with just a small set of nicely orthogonal methods with completely standard names.
Second, the use of higher order structures like Functor, Applicative, Monad. These get a LOT of airtime because they're both challenging and offer important capabilities. But they're also in a lot of senses their own realm of study. Not only are they developed very uniquely in programming communities (as opposed to what you'll find if you read about the category theoretic definitions) but they are also "higher order" in that they involve functions between types.
This higher-order nature both makes their own equations much more complex, but it also means that to see them applying (in languages other than Haskell and its ilk where purity drives this) you have to get really good at seeing languages in an abstract fashion. It's a great skill to develop, but ramps up the difficulty greatly.
Master the "first order" ones first. Master concrete, interesting types like Either, Maybe, List (as a source of non-determinism) first. Then come back and see if you can see how the skills you develop with the "first order" structures apply to these higher order, computationally minded types.
I actually wouldn't call it an algebraic structure at all. A functor really isn't just a set with some finitary operations. A quick search online [1] tells me I'm not alone.
https://www.quora.com/Why-is-functor-considered-to-be-an-alg...
This is the original use for monads in universal algebra, before they were interpreted as computational effects. An algebraic structure like a group or monoid is traditionally given by a signature: a list of what operations it has and what rules the operations need to follow. You can generalize from a signature Sig to a monad M. If a is a set, then Ma is "the set of expressions with constants from a": the set of formal expressions built from elements of a and the operations in Sig and where two expressions are regarded as equal if you can manipulate one into the other using the rules from Sig. The list monad for example corresponds to the signature for monoids.
The monad laws can thus be read as expressing "how to do algebra" at the most general level (ie. the level that is common to all algebraic structures). I would gloss them as "the order you evaluate an expression does not matter".
Generally, we're all talking about the same sort of thing. But also, generally, there isn't one good formal apparatus for discussing the whole of these things without inducing just tons of complexity. Why? Because we want to talk about specific and complex things and we often want to use specific and complex language as opposed to working through 8 levels of encoding every time.
So anyway, right there with you! But also happy to guide people down that path step-by-step.
The two main points (they're in the title) are eliminating the "von Neumann bottleneck" between the CPU and RAM, and the algebra of [FP] programs, the potential to manipulate programs as one manipulates mathematical formulas.
Indeed one of the complaints you sometimes see about functional programming is the amount of memory churn it produces on a Von Newmann architecture.
https://en.wikipedia.org/wiki/Function-level_programming
it's different
However, if I happen to bump into this need, I’d focus first on Logic and Array-Oriented programming languages first. They seem more valuable to my industry (finance).
For example: Prolog and J.
> But the one on the left will be slower and use a lot more memory.
Is it really true? I mean, GC will clean the intermediate array, won't it? And the speed won't be significantly slower. It's still linear complexity anyway.
const a = [...Array(1000000).keys()];
const m = 8;
let leftAvg = .0;
for(let _ of Array(m)) {
const t0 = performance.now();
a.map(Math.tan).map(Math.sin);
const t1 = performance.now();
leftAvg += (t1 - t0)/m;
}
let rightAvg = .0;
for(let _ of Array(m)) {
const t0 = performance.now();
a.map(x => Math.sin(Math.tan(x)));
const t1 = performance.now();
rightAvg += (t1 - t0)/m;
}
console.log(leftAvg, rightAvg, leftAvg/rightAvg);
// JS Firefox 70: 264 360.75 0.7318087318087318
var a = Enumerable.Range(0, 10000000).Select(x => (double)x).ToArray();
double[] xs1 = null;
double[] xs2 = null;
var m = 16;
var leftAvg = .0;
foreach (var _ in Enumerable.Range(0, m))
{
var watch = System.Diagnostics.Stopwatch.StartNew();
xs1 = a.Select(Math.Tan).ToArray().Select(Math.Sin).ToArray();
watch.Stop();
leftAvg += (double)watch.ElapsedMilliseconds / m;
}
var rightAvg = .0;
foreach (var _ in Enumerable.Range(0, m))
{
var watch = System.Diagnostics.Stopwatch.StartNew();
xs2 = a.Select(x => Math.Sin(Math.Tan(x))).ToArray();
watch.Stop();
rightAvg += (double)watch.ElapsedMilliseconds / m;
}
Console.WriteLine($"{leftAvg} {rightAvg} {leftAvg / rightAvg}");
// C# Results: 505.75 602.25 0.839767538397675It also matters how this code is compiled exactly. The C# version (I know nothing about C# or how good its compiler is) looks like it must first allocate some kind of dynamic stream, and only when ToArray() is called can it allocate the final array, so there might be extra copying. Maybe the compiler is smart enough to optimize a sequence of arr.Select().ToArray() to allocate a target array of the size of arr right away, I don't know.
Also, the JavaScript version uses a smaller array than the C# version, is that on purpose? 1000000 unboxed doubles are only 8 MB, which is not very big: On the machine I'm typing this on, L3 cache is 6 MB.
My advice would be to run the JavaScript version many times, for many more than 8 iterations, and with sizes increasing stepwise up to a GB or so. Also try replacing the maps with preallocated arrays and hand-written loops that contain only the computations, not the allocations. I know this sounds like I'm trying to give you homework, which I'm not, but benchmarking is hard, and there are many factors to take into account.
Looks like I was wrong about this! You might want to retry your experiments with cheaper operations than sin and tan.
I wrote a little C benchmark to test this more:
#include <stdio.h>
#include <time.h>
#include <math.h>
extern void sinTanSeparate(double *a, double *b, int n) {
for (int i = 0; i < n; i++) {
b[i] = tan(a[i]);
}
for (int i = 0; i < n; i++) {
b[i] = sin(b[i]);
}
}
extern void sinTanFused(double *a, double *b, int n) {
for (int i = 0; i < n; i++) {
b[i] = sin(tan(a[i]));
}
}
#define N (128 * 1024 * 1024)
#define RUNS 5
double a[N];
double b[N];
int main(void) {
clock_t start, end;
printf("will do %d runs over %zu MB of data\n\n",
RUNS, sizeof a / (1024 * 1024));
for (int i = 0; i < RUNS; i++) {
start = clock();
sinTanSeparate(a, b, N);
end = clock();
printf("separate: %f sec\n", ((double) end - start) / CLOCKS_PER_SEC);
}
printf("\n");
for (int i = 0; i < RUNS; i++) {
start = clock();
sinTanFused(a, b, N);
end = clock();
printf("fused: %f sec\n", ((double) end - start) / CLOCKS_PER_SEC);
}
return 0;
}
Compiling this with gcc -O3 gives: will do 5 runs over 1024 MB of data
separate: 1.461349 sec
separate: 1.020120 sec
separate: 1.019002 sec
separate: 1.019888 sec
separate: 1.018454 sec
fused: 1.014774 sec
fused: 1.014724 sec
fused: 1.013895 sec
fused: 1.016440 sec
fused: 1.013729 sec
So almost no difference, though with enough runs I think this would be significant. Interestingly, although C is not JIT compiled, even here there is a "warmup" effect. I guess these are initial page faults or something.But if we now comment out <math.h> and instead use some cheap "fake" implementations of in and tan:
// #include <math.h>
#define tan(x) (x + 1)
#define sin(x) (x + 2)
we get very different behavior: will do 5 runs over 1024 MB of data
separate: 0.548558 sec
separate: 0.154741 sec
separate: 0.151271 sec
separate: 0.150542 sec
separate: 0.151337 sec
fused: 0.078880 sec
fused: 0.074742 sec
fused: 0.078313 sec
fused: 0.076987 sec
fused: 0.077729 sec
Here the computation is so cheap that it's really other effects that dominate, and you get a 2x difference.99% of problems one encounters while programming can be solved in C++ with std::vector and functions taking a vector in and producing a vector out. That's my main problem with FP and many other language making bold claims: oversell. That simple fact is that for most computing tasks, you don't meed much more than simple types.
Sure. I might be using a monad and the bind function every single day, but acknowledging any useful repetition of patterns is never useful. That's why no successful programmer has ever even given a thought to design patterns.
/s
Why is it that people are alright with identifying and naming the pattern of a single global instance of a type - i.e. a singleton, but as soon as you identify a pattern of type signatures you're instantly thought of as looney and overselling?
Honestly, you don't need much more than a really big chalk board to solve most problems one encounters when programming.
Even that's probably strictly optional if you get enough people to double check the work.
Sarcasm aside, what you're often doing when you make these transformations of std::vectors are structured algorithms. The structures of these algorithms can often be factored through operations performed on types—even if they're just all different names for the same std::vector!
These structures exist to give words and patterns to the stuff we do. To make it easier to talk about, share, reflect upon, improve. To make it easier to judge how different approaches to the same end relate and can improve upon one another.
You don't have to go use Haskell to get a LOT of benefit out of algebraic and equational reasoning. And I feel pretty unsure what to think about a philosophy where one would avoid a nice tool merely because it's possible to get along without it?
I imagine you're a fan of that Primitive Technology YouTube channel?
1. I think there is value in immutable data structures. Languages like Haskell and Elm make everything immutable by default. A big problem I find in C#, JavaScript (and I'm sure you'd get in C++) is if I return a List of something, even if the list is readonly, the consumer could modify the items, unless I go to some tedious lengths to make sure the list only contains immutable objects. Also if I compute something based on a reference to an object, I cannot be sure that what the reference represents hasn't changed later in the program.
2. The algebraic data type produces very nice tight data structures and makes it easier to create data structures that can't hold invalid values. And where this is not possible you can use techniques to hide/product the data - usually by hiding the constructor, and requiring a function to make the type. You can do this in OO with classes, but the fact that any reference can be null causes issues, also if you return anything from your class that isn't immutable then you are passing out a potential backdoor to f' your state. See React and the hoops you have to jump through to keep things immutable.
You can absolutely get stuff done without FP, but I like the abstractions and guarantees it brings. Less to go wrong, and less to think about overall (once you have carefully planned your types).
What I don't like about FP, or Haskell in particular is the very complex types people dream up and all the crazy GHC extensions which are hard to understand and produce the most unhelpful of error messages if you make a mistake. Elm is more my kind of thing - very simple type system but still algebraic, no forced nulls etc. Gives you the 80% benefit of FP for 20% of the effort.
When I read an Elm program from someone else I don't have to think much. Reading a Haskell program I need to learn a lot to understand it. Maybe Haskell is OK if you are doing it full time and can commit all that stuff to long term memory.
If you seek to solve a problem with a solution that has the best design, the least technical debt and almost no bugs. Then FP with ADTs is the closest thing I've encountered to such a solution.