DEV Community

loading...

Discussion on: OOP vs functional programming

Collapse
skyjur profile image
Ski

IMHO currying in javascript only makes things worse

I don't need to curry in order to make anything more composable. I could always compose simply by declaring new function

const add = (a, b) => a+b
const mul = (a, b) => a* b

const composed = (a) => 
   add(add(a, 1), mul(a, 2))

number of things is better in this approach over curried version:

  • I can tell from syntax how many arguments composed function takes
  • I don't need helpers for composition
  • when binding arguments it's not guaranteed that one always wants to bind arguments in same order as it's defined in curried function
  • I know for sure just from syntax that the the end result is a function not object or result or anything else
  • debugging is easier (I can put breakpoint or log statements and inspect where variables go and what value they have)

So in nutshell currying to me is a solution for non existing problem which makes things worse when applied religiously.

In my initial example I did used one case of a 2nd order function, which was a case of curried function. But this was done in order to separate dependencies from data. I would do that only if I know usage patterns

const createFunction = (dependencies) => {  // usually other 2nd order functions here
   (data) => { // data objects here
   }
}

This implies that intended use of it is of is to be instantiated before usage.

const someFunction = createFunction()
someFunction()

and definitely not createFunction()() - if there is a case to use it like this then it's better to declare it with flat arguments.

To me problems with this functional approach and composed functions happen when composed function must be passed down as parameter. This is where to me class based approach wins over function based approach. After function is passed as argument, it becomes more difficult to trace which code is relevant when function is executed. It's not necessary a problem when writing a code but it's a problem when you are in large code base and want to understand it. With typescript can declare interface of function but that does not help finding implementation. On other hand in class based approach receiver can indicate concrete class that it expects (even without typescript - with js docs).

Thread Thread
peerreynders profile image
peerreynders

Your initial composing function

const createComposedOperation(op1, op2) =>
    (a, b) =>
        op1(a) + op2(b)

accepted arbitrary single argument functions.

Now your preference

const composed = (a) => 
   add(add(a, 1), mul(a, 2))

is to manually assemble the function.

I was referring to generalized function composition. Given that functions only return a single value general function composition can only compose functions that accept a single value. In that context currying is the workaround to fake multi argument functions.

I wasn't advocating "curry all the things" just for the sake of it.

This style

const createFunction = (dependencies) => {
  (data) => {
    // some code here
  }
}

is related to

const createFunction = (a, c) => {
  (b, d) => fn(a,b,c,d);
};

i.e. using a closure to mimic partial application - it's just that "some code here" is never isolated into an independent function.

Your particular annotation identifies dependencies as arguments to a kind of constructor: "A closure is an object that supports exactly one method: apply."

After function is passed as argument, it becomes more difficult to trace which code is relevant when function is executed.

Passing a function as an argument to a higher order function is equivalent to passing a strategy object to a context object (Strategy Pattern) - i.e. this kind of composition exists in both paradigms.

With TypeScript can declare interface of function but that does not help finding implementation. On other hand in class based approach receiver can indicate concrete class that it expects. On other hand in class based approach receiver can indicate concrete class that it expects.

What you are saying is that you find it more difficult to work with interfaces than concrete implementations (i.e. this isn't about functions vs. objects). That may be so but:

"Design Patterns: Elements of Reusable Object-Oriented Software" p.18

  1. Clients remain unaware of the specific types of objects they use, as long as the objects adhere to the interface that clients expect.
  2. Clients remain unaware of the classes that implement these objects. Clients only know about the abstract class(es) defining the interface.

This so greatly reduces implementation dependencies between subsystems that it leads to the following principle of reusable object-oriented design:

Program to an interface, not an implementation.

i.e. classes depending on other concrete classes should be the exception, not the rule.

The natural boundary around a class that depends on other "concrete classes" automatically includes those "concrete dependencies" (and recursively their concrete dependencies). This creates a much larger unit that needs to be "reasoned about" as a whole.

Interfaces are at the core of many OO practices including the dependency inversion principle.

You probably have other, bigger problems in the code base when you have difficulty tracking down the concrete implementation that is used to service a particular interface at a call site.

It's also a running joke that function types satisfy the interface segregation principle by default.


The impression I'm getting here is that you find imperative code ("do this then that") easier to read - which isn't surprising given that most of us learn programming that way - it's the allure of the familiar.

Functional programming tends to focus less on the "how" and more on the "why" and "what" (some say it's more declarative) - but it still has its 'step-by-step' moments. For example:

// transform the string to an {ok/err} result
// use an IIFE to initialize function closure
const transform = (() => {
  const fn0 = validateRequired('Please provide a value');
  const fn1 = andThen(validateInteger('Please provide a integer'));
  const fn2 = map(n => Number.parseInt(n, 10));
  const fn3 = andThen(validateBetween('Please specify an integer between 0 and 18', 0, 18));
  const fn4 = map(factorial);

  return value => fn4(fn3(fn2(fn1(fn0(value)))));
})();

Now transform is a terrible name, even textToFactorial would have been better but at the time I was trying to make a general point. But the "steps" are still there, clearly outlining what is going on. The big difference (to imperative code) is that this code isn't transforming any data at this point - the function that will be transforming the data is being "wired up".

Functional code is composed of functions and as functions are generally smaller than objects there will be proportionally more code dedicated to "wiring up" the capability rather than "doing" the capability. So when reading the code one has to differentiate between "construction" code (the scaffolding) and "running" code.

But the same is true for any non-trivial object-oriented code base. As it grows and God Objects are avoided more and more code is dedicated to setting up the relationships between the collaborating objects before they can do any useful work.

However a network of interacting stateful objects can grow in complexity rapidly. A composition of stateless functions (or immutable closures) is typically easier to reason about. Coming from an imperative background the functional approach is different enough to take some getting used to.

(One issue with React hooks is that functional components are a now just as stateful as objects - which gives rise to much richer (i.e. complex) behaviour - the standing argument is that hooks are more declarative than object methods but that is a whole discussion its own).

Thread Thread
skyjur profile image
Ski

You probably have other, bigger problems in the code base when you have difficulty tracking down the concrete implementation that is used to service a particular interface at a call site.

It's not that I have difficulty. It's just not as fast. With OOP when coding on top of interfaces tooling supports jump-to-implementation. It's just less straight forward with function types.

I do not advocate towards breaking any best practices. That said not every practice that is necessary when designing reusable code is also necessary or even good when building one off pieces of implementation.

Passing a function as an argument to a higher order function is equivalent to passing a strategy object to a context object

Strategy is just one use case. There are many cases. In UI applications hardly anything passed down element tree is 1st order function. If it was 1st order most of time you'd not pass it down - it can be imported and called directly.

The impression I'm getting here is that you find imperative code ("do this then that") easier to read - which isn't surprising given that most of us learn programming that way - it's the allure of the familiar.

Well firstly I think it's little bit condescending for you to make impression of why I "find imperative code easier to read".

But regardless of that, I think this is a terrible argument. Normally if someone does not find my code immediately straight forward I tend to believe that I made it too difficult. Sometimes I just couldn't think of easier way. Sometimes simply because I did something I thought was clever - turns out it wasn't.

Now to your example. If you're writing in this style then at least you could get rid of fn1, fn2, etc and that closure pattern - all that make it very ugly

const transform = createFlow([
   validateRequired('Please provide a value');
   andThen(validateInteger('Please provide a integer'));
   map(n => Number.parseInt(n, 10));
   andThen(validateBetween('Please specify an integer between 0 and 18', 0, 18));
   map(factorial);
])

to me very big downside of this code is that it's very hard to use debugger on it. Almost in no point can you place a breakpoint on it except for parseInt() part

And what is reason for it? I don't think there is good one. It's very imperative style in the end. Why would you write imperative code in declarative-functional-composition? Just makes no sense. There are situations where declarative style is great. There are situations where imperative style is great. Use one that is the most appropriate. Application like this would be best split into input, validation, action phases. Validator can be setup in declarative way. The rest can go in imperative. And result is best of both worlds, easy to follow, easy to debug.

const validate = number.required.between(0, 18)

function app(data) {
   const [value, error] = validate(data)
   if(error) return [null, error]
   return [factorial(value), null]
}
Thread Thread
peerreynders profile image
peerreynders

In UI applications hardly anything passed down element tree is 1st order function.

Not sure where this is going.
As far as I'm aware "higher order function" doesn't imply an actual ordinality.
Eric Elliot used first order function to refer to a function that doesn't "take a function as an argument or return a function as output". Eric Normand on the other hand uses:

  • zero-order function - a function that takes values, non-functions
  • first-order function - a function that takes a function
  • second order function - a function that takes a function that takes a function.

So I can only conclude that the "n-th order function" terminology with reference to higher order functions is neither standardized nor commonplace.

"Higher order function" simply calls attention to the fact that a function is specialized by and/or returns another function. But in the end in the functional style it is as natural to return functions and take them as arguments as it is to return an object and take object arguments in the object style.

The impression I'm getting is that you're saying that "a function that takes a function that takes a function" (and beyond) is getting hard to track. But given that functions have a type, a function can simply transform one type of function into another type of function.

I think it's little bit condescending

No condescension was implied. The point was familiarity bias - Rich Hickey style i.e. easy is a result of familiarity, not simplicity and the unfamiliar can seem difficult even when it's simple.

At its core JavaScript is an imperative language - it just happens to also have first class functions and supports closures which can be leveraged when practicing a functional style.

And given that TypeScript keeps coming up, it's been my observation that TypeScript is much less conducive to enabling a functional style than JavaScript - to the point that it could be argued that TypeScript is the "wrong tool for the job" to support a statically typed functional style. I know of fp-ts but you have to work much harder in TypeScript to practice a functional style compared to an object style. But that's not the fault of the "functional style" but a result of TypeScript being streamlined for OO style typing.

at least you could get rid of fn1, fn2, etc and that closure pattern - all that make it very ugly

That "ugliness" exists for illustration purposes - being explicit about the bound values being functions and the manner in which those functions are being composed.

it's very hard to use debugger on it.

That argument keeps coming up. You can still set a breakpoint at any function declaration and you can run into similar problems with dynamically assembled objects.

One could just as easily argue that it is disappointing that

  1. debugging tools make this hard
  2. we to this day have such a heavy reliance on debuggers given code reviews, automated unit tests, linters, and static type checkers ("I do not use a debugger" - debuggers have their place but some people seem to use it like this - i.e. debugger guided program construction).

I think the "hard to debug" argument has even more of a negative impact when it comes to adoption of streams which could potentially simplify UI architecture or perhaps enable some alternate approaches.

Why would you write imperative code in declarative-functional-composition?

The code isn't imperative, the style is. Even Haskell has the do notation ("Haskell is the world’s finest imperative programming language").
Some people find it more intention-revealing.

The functions are organized in the run-time sequence of the composed function so that it's clear what the transform will do. The {ok/err} type implements the necessary Railway-Oriented Programming (via andThen and map).

easy to follow, easy to debug.

To me that sample code seems influenced by Go:

"It must be familiar, roughly C-like. Programmers working at Google are early in their careers and are most familiar with procedural languages, particularly from the C family. The need to get programmers productive quickly in a new language means that the language cannot be too radical."

There's anecdotal evidence that something like How to Design Programs (HtDP 2e) may be a better first exposure to programming: “It’s mind boggling that your HtDP students are better C++ problem solvers than people who went through the C++ course already”.

The Structure and Interpretation of the Computer Science Curriculum

Thread Thread
skyjur profile image
Ski

Well this is going deeper and deeper into rabbit hole.

The only thing I am trying to do is point out what sort of things work and what don't work in JavaScript (or TypeScript). Meanwhile you're pointing to a lot of deep topics that I don't necessary find that relevant.

I do not stand against using functional languages and transpiling it to js. If someone wants functional programming - this is what I would suggest.

And I tell that things not work not because I am unfamiliar. So far every single topic that you mentioned either I tried my self or received in codebase that I had to work with and I suffered. I also coded bit in scala, elixir and practiced solving problems in functional manner thus functional concepts are not new to me.

The fact for example that railway programming exists does not mean that it's good technique in JavaScript (most often it isn't). Even in F# same author that you shared wrote a more skeptical piece about it 6y later more recent piece

Regarding discrediting the use of debuggers. Some people don't use debuggers. That does not change fact that for a lot of people it's a time saving tool in every day job. I have learned to use debugger many years later than I started coding thus to me personally it's less important - yet still in multiple cases it was very time saving tool. But I also met people who learned to use it from very first days they started learning coding and I don't think that telling them that now they should stop using debugger and learn "the way real men are programming" and send them a link to what Linus Torvalds thinks of debuggers. I don't think this gonna help them do their job better. What will make things better is code that is easy to debug, straight forward and use only the minimal amount of concepts necessary to solve the problem.

But argument about being able to use debugger often touches on other aspects. Usually if it's hard to use debugger you also have muffled tracebacks and application flow that is very hard to follow. Thus it's very good idea to stay aware of things that break debugging and tracebacks.

On topic of tracebacks. I will bring again the case of currying. Here is example with curried version

const createSomeHandler = something => event => { ... }
render() {
  const onHandle = createSomeHandler(something)   
  <Child onHandle={onHandle} />
}

If stuff breaks in onHandle, the render here is not in the traceback despite it being responsible for wiring things up (binding something to the handler).

Same handler might been used in 100 other places. And without traceback pointing to above will be hard to find.

And it is so easy to make things much better

handle = (event, something) => { ... }
render() {
  const onHandle = event => handle(event, something)   
  <Child onHandle={onHandle} />
}

now if onHandle fails, 'render' function is in the traceback and you immediately know what exactly failed.

When you get email with error and traceback with a new bug in production this very simple thing could change situation from you not having any idea where to start at to knowing exactly what went wrong.

You also mentioned piece about unfamiliar bias. It's still never a good idea at any point to say to anyone "maybe you find this hard because you are unfamiliar". Instead you could nudge them in right direction and they might realize that they been lacking familiarity. It could very well turn out that you your self was the one unfamiliar about something. Thus it's just good conversation tactics to keep it to your self even if you think that way and see where things will roll.

Thread Thread
peerreynders profile image
peerreynders

Well this is going deeper and deeper into rabbit hole.

It started with your initial statement:

To me weak spot of functional code in js is composition.

leading up to

and all you see when opening some deep module is this

Polymorphic code in OO has exactly the same problem - and if you're unlucky, even if you locate the implementing class, you may still have to drill through several layers of implementation inheritance to find the code that is actually running - i.e. it's the type of code where even with a debugger you wonder - how did I even get here? And even if the polymorphic type on the parameter is declared the implementing type doesn't have to declare it due to structural typing in TypeScript (because of duck typing in JavaScript).

In the end functional composition or more specifically composing closures works just fine in JavaScript - and really isn't that different from composing objects. The difference is that closures are implicit while objects are explicit - which can make closures more difficult to grasp initially but objects can be more verbose.


The point of the original article was to contrast OO vs functional style.

The functional approach focuses on values and how you transform the values you have into the ones you need. The building blocks are functions and their behaviour can be composed through higher order functions.

Object-orientation is based on partitioning the solution in terms of Class-Responsibility-Collaborator and using commonality and variability analysis to identify opportunities for polymorphism.

Which approach sounds simpler now? It's this type of comparison that leads to statements like:

"OO makes code understandable by encapsulating moving parts. FP makes code understandable by minimizing moving parts."
Michael Feathers

Another relevant sound bite:
"GOTO was evil because we asked, 'how did I get to this point of execution?' Mutability leaves us with, 'how did I get to this state?'"
Jessica Kerr

Eliminating mutability in JavaScript is unreasonable but it makes sense to try to find approaches to "use mutability responsibly" whether you are using objects or closures. Both mutable objects and closures should be used with care.

Object Thinking, Functional Thinking, and Reactive Thinking are very different, each useful in their own way.

what don't work in JavaScript (or TypeScript).

The point is that there are things that work in JavaScript that don't sit well with TypeScript as its design is much more OO centric than JavaScript's. When you use TypeScript you are effectively choosing OO - with a functional style your are constantly fighting an "impedance mismatch".

While TypeScript is superset from a feature support perspective, it becomes a subset once one clamps down on its flavour of type checking.

The fact for example that railway programming exists does not mean that it's good technique in JavaScript (most often it isn't).

JavaScript has exceptions and they exist for a reason. But there are always "expected errors" and those shouldn't be handled via exceptions. The whole idea is that map and the like remove the need for those noisy if(error) return [null, error] statements all through the application code - if that isn't an acceptable tradeoff then use something else.

Regarding discrediting the use of debuggers.

Given an approach with promise and merit:

  • People won't adopt it until there is "better tooling" to support it.
  • "Better tooling"" isn't developed because adoption is low.

That's the trap that "hard to debug" is a part of.

Meanwhile once there is enough tooling even mediocre solutions can gain momentum. TypeScript was languishing until VS Code came along.

I will bring again the case of currying.

That behaviour is common for any function that creates a function. Your second example creates a closure inside of render() so onHandle will appear in the stack trace.

So if that stack trace is important to you create the intermediate closure:

function createSomeHandler(something) {
  return function handler(event) {
     throw new Error("Boom");
  };
}

function render() {
  const fn = createSomeHandler("something");
  const onHandle = e => fn(e);
  setTimeout(onHandle, 100, "event");
}

render();
/*
  VM692:3 Uncaught Error: Boom
    at handler (<anonymous>:3:12)
    at onHandle (<anonymous>:9:25)
 */
Enter fullscreen mode Exit fullscreen mode

Instead you could nudge them in right direction and they might realize that they been lacking familiarity.

It's been my experience that people tend to view "difficulty" as something that is inherent to a subject - not something that derives from their own experience. The question "Does this only seem difficult simply because I haven't done this before?" isn't asked nearly enough.

Thread Thread
skyjur profile image
Ski

IMHO this whole juxtaposition of OOP and functional ideologies doesn't really make much sense to me.

My impression was that article expressed similar idea - that you can use OO and functional techniques when it makes sense.

When I say that I prefer class-based composition over higher-order-function based composition I do not stand that I support OOP paradigm and reject functional paradigm.

I prefer classes because they work as anchor points for documentation, by providing a referable name that can be used with TypeScript or without TypeScript (with jsdoc) and thus making code easier to understand and navigate. Higher-order-functions are lacking this. If someone can figure this part out for higher-order-functions then I would happily use it.

"OO makes code understandable by encapsulating moving parts. FP makes code understandable by minimizing moving parts." Michael Feathers

The only takeaway I can do here is that both things are equally important

  • encapsulating moving parts
  • minimizing moving parts

Focusing only on 1 will lead to problems.

Some concepts that you bring - I don't see any reason to classify as "functional" or "OOP". For example railway programming - it's a technique for error handling. There is nothing about it that makes it "functional". It plays nicely if pattern matching is supported as 1st class citizen. But one can also use idea to implement this concept with OO. If it's practical to do so is another question. I'd lean towards saying that "everything has known error" is incorrect assumption - only on application boundaries there are known errors (data inputs, networking), within application there usually are no errors - if there are - then maybe it's a time to review architecture.

Polymorphic code in OO has exactly the same problem - and if you're unlucky, even if you locate the implementing class, you may still have to drill through several layers of implementation inheritance to find the code that is actually running - i.e. it's the type of code where even with a debugger you wonder - how did I even get here?

I don't think highly polymoprhic code is that normal. It's often a case of inheritance abuse. And then the more layers you have the stronger my point gets because there is no reason why higher-order-function based solution would have less layers than that of class-based solution, and with every single layer every time it's easier to navigate the code.

The point is that there are things that work in JavaScript that don't sit well with TypeScript as its design is much more OO centric than JavaScript's. When you use TypeScript you are effectively choosing OO - with a functional style your are constantly fighting an "impedance mismatch".

How could anyone consider JavaScript "functional" is something I don't quite understand. It came out same decade as Python and Ruby and all 3 have a lot in common. All 3 implement closures very similarly - similarly like it was done in Smalltalk - a precursor to all OO dynamic languages. All have OO model at it's core. Even a Function in JavaScript is a callable object - same as Python by the way ((function(){}) instanceof Object => true).

I can't think of good example of what works well with JS but not TypeScript thus I'm not entirely sure what you're pointing to.

I believe reason why some piece of code won't work with TypeScript is not because it's not OO but because it's "too dynamic".

But I think this sort of code tends to not work not only with TypeScript but also with all other JS ecosystem (js runtime engines, jsdocs, linters).

Meanwhile once there is enough tooling even mediocre solutions can gain momentum. TypeScript was languishing until VS Code came along.

It's never enough to bring in a concept but not solve the tooling problem. Thinking about tooling first also ensures that concept will play well with tooling. Some concepts make tooling practically impossible to build.

TypeScript from it's beginning was built thinking about tooling - from first days it come with language server that integrates seamlessly into any IDE. TypeScript became best language server available for javascript even if one is not using TypeScript. VSCode also became biggest open-source project in javascript ecosystem thus it showed cased how to solve scalability problems of javascript - with typescript. I totally agree that tooling will effect how solutions are created. In this case not exactly because of vscode, but because of typescript language server. Capability to produce tools always had and always will dictate what approach is taken and not purely theoretical background. We might like it might not but it's a simply a facts. This is true in every engineering field. When you write code for your own personal project you can explore anything you want. When you write code for it to serve a business together with many other practicing developers your goal is no longer to explore solution scope but to produce a solution that is well accepted in industry - and that means it a solution that plays well with existing tools - not hypothetical tools.

Thread Thread
peerreynders profile image
peerreynders

you can use OO and functional techniques when it makes sense.

The article uses "vs" and supplies two distinct implementations (though the first example is largely procedural). To highlight differences it's typical to take the examples to the extreme.

Of course there is a range of possible solutions between both extremes.

You seem to prefer leaning toward the OO end of the spectrum.

My contention is that JavaScript naturally tends more towards the function-based end (not necessarily to the extreme of my "function-style" example).

by providing a referable name that can be used with TypeScript or without TypeScript (with jsdoc) and thus making code easier to understand and navigate.

// a function that takes any two arguments of the same type 
// and returns a result of that type
type Op2Args<T> = (a0: T, a1: T) => T;

const add: Op2Args<number> = (augend, addend) => augend + addend;

const sub = (minuend: number, subtrahend: number) => minuend - subtrahend;

function mult(multiplicand: number, multiplier: number) {
  return multiplicand * multiplier; 
};

function run(f: Op2Args<number>, a: number, b: number) {
  return f(a, b);
}

const runOp: (op: Op2Args<number>) => void = 
  op => run(op, 2, 3);

console.log(runOp(add));  //  5
console.log(runOp(sub));  // -1
console.log(runOp(mult)); //  6

In the above example Op2Args is a referable name - but from your past comments you don't like this because at the definition site of f it isn't obvious that either add, sub or mult might be used.

both things are equally important

As stated "minimizing moving parts" has priority (as long as all the tradeoffs are acceptable) over "encapsulating moving parts". Without minimizing the moving parts first, "accidental complexity" tends to be entombed into the encapsulation. But that is true of any approach.

For example railway programming

is considered functional because "mapping a function over a type" is extremely common in functional programming - in an OOP without first class functions you would have to "map an object over another object" where the former has to implement a method (i.e. a single method interface) the latter is requiring - with is way more convoluted.

I don't think highly polymorphic code is that normal.

Polymorphism is everywhere.

interface Node extends EventTarget {
  //...
  appendChild<T extends Node>(newChild: T): T;
  //...
}

i.e. appendChild will append anything that implements the Node interface - that could be a lot of different kinds of objects.

In the example above Op2Args makes f polymorphic.

React uses React.Component so that it can treat all your components in an identical manner - that is polymorphism in action.

Python

Guido van Rossum is anti-functional - lambda almost didn't make the cut.

All 3 implement closures very similarly - similarly like it was done in Smalltalk - a precursor to all OO dynamic languages.

Closures were devised by Peter Landin in 1964 as described in "The Mechanical Evaluation of Expressions" (lambda calculus). That is why closures are considered a "functional" feature - regardless of where they may have been adopted later.

All have OO model at its core.

If you were talking about TypeScript I would agree - it's opinionated towards class-based OO. But in the case of JavaScript I disagree and I've elaborated on that here.

While being essentially imperative the core building block of JavaScript is the function. Multiple functions can share state through a shared closure so that these functions may act as methods. That is essentially how OOP with functions works.

The object - in an "object-oriented" (but not class-based) sense - is an emergent concept via the function context - this giving a function the capability to access other values on the "object" the function was being referenced through.

The prototype chain then makes it possible to share a function across object instances (to save memory). So conceptually object instances sharing the same function across the prototype chain belong to the same "class" - but there is no class construct, only object instances.

Given the function first nature of JavaScript, OO workarounds like the Command Pattern (as in Replace Conditional Dispatcher with Command) aren't necessary - just pass a function with its attached closure.

I can't think of good example of what works well with JS but not TypeScript.

I'm referring to the lopsided "typing tax" that TypeScript imposes on "function-style" vs "class-style" (or "procedural-style") code that you yourself already commented on. If one persists on using function and closure based approaches in TypeScript one quickly finds oneself knee deep in noisy type definitions expressed in TypeScript's typing meta language. Functional languages already devised concise ways of expressing function types.

With "class-style" code you can coast on rudimentary and terse typing features for a long time before you ever have to dive into the advanced types. But making it easy to work with function and closure types wasn't a priority - despite the fact that these are core JavaScript features.

Going by the mantra "make doing the right things easy and the wrong things hard" by focusing on making "class-style" code easier to type, TypeScript reinforces the idea that "class-based object-orientation" is the "right thing" while "function-style" is the wrong thing.

TypeScript admits as much:

TypeScript began its life as an attempt to bring traditional object-oriented types to JavaScript so that the programmers at Microsoft could bring traditional object-oriented programs to the web. ... The resulting system is powerful, interesting and messy.

In 2008 Douglas Crockford wrote:

JavaScript is most despised because it isn’t some other language. If you are good in some other language and you have to program in an environment that only supports JavaScript, then you are forced to use JavaScript, and that is annoying. Most people in that situation don’t even bother to learn JavaScript first, and then they are surprised when JavaScript turns out to have significant differences from the some other language they would rather be using, and that those differences matter (JtGP, p.2).

"TypeScript the Good Parts" focuses on "class-based object-orientation" much the same way that "JavaScript the Good Parts" doesn't. In a strange twist TypeScript has become that other language that most people would rather be using so that they don't have to bother learning JavaScript first.

It's never enough to bring in a concept but not solve the tooling problem.

Oliver Steele made an interesting distinction between "Language Mavens" and "Tool Mavens" in IDE divide - he comes to the conclusion that tool-orientation comes at the cost of language features.

Ironically Java has been continually ridiculed for its need of tooling to "keep developers productive" in the face of the all the language's and ecosystem's warts. Meanwhile it's perfectly acceptable for the members of the JavaScript community to employ tool heavy build pipelines (editors requiring language servers) and constantly clamour for "better tools".

Thread Thread
skyjur profile image
Ski

I have read JavaScript the Good-Parts. Haven't yet had chance to read the similarly named one on TypeScript.

On function returning objects through closures as means of OO there are benefits and weaknesses compared to class (old prototype based) approach.

function MyObj {
  var name = 'something'

  return {
    get name() { return name }
  }
}

I think it's not a good approach. I think Crockfords originally didn't take all important aspects into consideration.

It's useable in isolated case but I believe it was never practical to accept this as a standard way to move forward with OO in JS.

I also believe even before introduction of 'class' it was never mainstream approach. The approach that was always considered "correct" was prototypal OO (check examplesf influencial js libraries such as dojo, prototype, jQuery v1 - most common are custom class builders due to verbosity of direct usage of prototype, or use of prototype directly)

function MyObj{}
MyObj.prototype.name = function() { }

And due to multitude of different approaches towards doing OO in JS, there was a need to have a standard. Class was a straight forward thing that is widely understood and played perfectly well with JS prototype thing.

The approach of doing OO through means of closures comes with some benefits over prototypal/class based approach

  • You don't need 'this' when writing the code
  • Truly private scope
  • methods are indefinitely are bound to closure, thus there is no risk of screwing this up

This can be worked without much difficulty in 'class' and it was already proven.

Regarding the private attributes

  • ES7 supports private members throughh #privateMember
  • there is static code checking available for jsdoc tag /** @private */

Regarding the this bounding problem

  • static type checking can report this error
  • onClick=e => myObject.onClick(e) solves this

I consider passing method from object as a function to always be a weak practice. If one wants to pass function - this is excellent use case for arrow function. Always create a new arrow function, if wanting to pass a function. Arrow functions are the link between functional and OO approaches. Using method without object (as in passing it as function) doesn't check out with core OO idea of message passing to object.

Weak points of this approach to do OO I think were much harder to iron out. It simply didn't played nicely with underlying language concepts that existed from 1st days of JavaScript (prototypes). Problems of this approach:

  • same thing as with higher-order-function this pattern does not declare a type thus there is no means of referring to it - you'd need to invent something here to be able to refer it (something like x' in ReScript)
  • there was no means of introspection (which was understood as important goal of dynamic language at certain point, however now with transpilers commonly used this might not be a needed feature at all)
  • certain performance implications - objects created through this way are more memory heavy and slower to instantiate, might again not matter that much today but was a factor to chose prototype over this method
  • most importantly: there is no means of type checking const x = MyObject(); x instanceof MyObject, keyword instanceof just no longer makes any sense, it's hard to move forward with a technique that doesn't play well with certain 1st class language features already in existence

I understand the point of not using classes, in favor of doing pure functional development in JS. I completely miss the point of trying to use functions for OO where classes are available to implement OO.

Thread Thread
skyjur profile image
Ski

Here is a case from 2009 that argued against
functional pattern proposed by Douglas Crockford bolinfest.com/javascript/inheritan...

Many arguments are Closure Compiler related. Now of course if to be purists arguments about specific tool maybe would not matter. But consider that job of developers is to produce working software. If you had to write a bigger piece of code in js then Closure Compiler was the tool to go with, it would be either impossible, or you'd need to invent your own tools.

Thread Thread
peerreynders profile image
peerreynders

Haven't yet had chance to read the similarly named one on TypeScript.

There isn't one.

But when Microsoft states "so that the programmers at Microsoft could bring traditional object-oriented programs to the web" they are talking about C# style class-based object-orientation.

TypeScript was designed with a good class-based OO development experience in mind - sacrificing the ease of other approaches that may be equally valid under JavaScript (which claims to be multi-paradigm).

I also believe even before introduction of 'class' it was never mainstream approach.

You are correct. Because mainstream OO is "class-based". "OOP with closures" is only about objects, not classes.

The approach that was always considered "correct" was prototypal OO.

I would argue that the mainstream didn't even accept "prototypal OO" as correct given that there is no explicit class mechanism and the confusion that it caused up to and including ES5. But it certainly is possible to emulate class-based OO with prototypal OO. Strictly speaking membership to a class should last for the lifetime of the object. JavaScript objects can be augmented after construction - so "class membership" isn't fixed.

Class was a straight forward thing that is widely understood and played perfectly well with JS prototype thing.

The important aspect was that it aligned with the mainstream mindset of class-based object-orientation. However the code before ES2015 wasn't straightforward.

some benefits

You missed:

  • Elimination of inheritance. Commonalities have to be managed entirely through composition.
  • Don't need new to instantiate an object (useful if instances are cached);

ES7 supports private members through #privateMember

They landed in Chrome 74 but as such the proposal has been stuck at stage 3 since 2017 - it didn't get into ES2020; maybe it will be part of ES2021 (ES2016 is ECMA-262 7ᵗʰ Edition).

Regarding the this bounding problem

The problem with this has more to do with developers from other languages not understanding how it works - that it is a deliberate decision not to bind the function to the object.

(something like x' in ReScript)

'x is a simply type variable just like T is a type variable in Op2Args<T>. And the object returned by the factory still has a structural type.

keyword instanceof just no longer makes any sense,

instanceof can be a great escape hatch but the whole point of polymorphism is that the object should know what to do without the type needing to be known first (Replace Conditional with Polymorphism).

I completely miss the point of trying to use functions for OO where classes are available to implement OO.

There isn't just one kind of object-orientation. You are correct that the mainstream assumes "class-based object-orientation" when OO is mentioned; really COP - class-oriented programming would have been a better name (the code is declaring classes, not assembling objects).

"OOP with closures" doesn't seek to emulate "class-based object-orientation". Without inheritance, composition is the only option which leads to a simpler style of object-orientation. Also the notion isn't that "closures are like classes" but that "closures are like objects".

Once "closures are like objects" sinks in, it should become apparent that there are situations where closures can be more succinct than objects (created by a class).

Also consider that in 2008 ES5 wasn't even finalized yet - class wasn't official until 2015.

Which one is easier to understand

// closures as objects
function phone(phoneNumber) {
  return {
    getPhoneNumber: getPhoneNumber,
    getDescription: getDescription
  };

  function getPhoneNumber() {
    return phoneNumber;
  }

  function getDescription() {
    return 'This is a phone that can make calls.';
  }
}

function smartPhone(phoneNumber, signature) {
  var core = phone(phoneNumber);
  signature = signature || 'sent from ' + core.getPhoneNumber();

  return {
    getPhoneNumber: core.getPhoneNumber,
    getDescription: getDescription,
    sendEmail: sendEmail
  };

  function getDescription() {
    return core.getDescription() + ' It can also send email messages';
  }

  function sendEmail(emailAddress, message) {
    console.log('To: ' + emailAddress + '\n' + message + '\n' + signature);
  }
}

Or this one?

// Combination inheritance
// prototype chaining + constructor stealing
//
function Phone(phoneNumber) {
  this.phoneNumber = phoneNumber;
}

function getPhoneNumber() {
  return this.phoneNumber;
}

function getDescription() {
  return 'This is a phone that can make calls.';
}

Phone.prototype.getPhoneNumber = getPhoneNumber;
Phone.prototype.getDescription = getDescription;


function SmartPhone(phoneNumber, signature) {
  Phone.call(this, phoneNumber); // inherit properties

  this.signature = signature || 'sent from ' + this.getPhoneNumber();
}
SmartPhone.prototype = new Phone(); // inherit methods

function sendEmail(emailAddress, message) {
  console.log('To: ' + emailAddress + '\n' + message + '\n' + this.signature);
}

function getDescriptionSmart() {
  var description = Phone.prototype.getDescription.call(this);
  return description + ' It can also send email messages';
}

SmartPhone.prototype.sendEmail = sendEmail;
SmartPhone.prototype.getDescription = getDescriptionSmart;

If you had to write a bigger piece of code in js then Closure Compiler was the tool to go with, it would be either impossible, or you'd need to invent your own tools.

Not everybody has Google size problems - and the tradeoffs of the closure-based approach are known.

Clearly in a project using the Closure compiler one would stick to the recommended coding practices. But when in the past I had a look at the Closure library it struck me that it was organized to appeal to Java programmers - so it's not that surprising that the compiler would favour the pseudo-classical approach (apart from being more optimizable).

In any case I'm not recommending ignoring class - just to be familiar with the closure based approach; it does exist in the wild and in some situations it could come in handy.