Is functional programming worth it?

Поделиться
HTML-код
  • Опубликовано: 26 июл 2023
  • I've been playing with functional programming and algebraic data types recently, and I'm not sure the trade offs are worth it.
    Code from the video: github.com/andrew8088/habits.sh
    My Links
    shaky.sh
    shaky.sh/tools
    / andrew8088
  • НаукаНаука

Комментарии • 69

  • @warrensadler1271
    @warrensadler1271 11 месяцев назад +14

    I think you did a good job of applying purify-ts! I would however not nest an Either in a Maybe as the two ideas are potentially redundant. Further, when it comes to applying structures like Either and Maybe, I'd prefer to use them at the edges of my system in the IO layer; LocalStorage was a wonderful example. Strong boundaries, beget simple logic. Also, I might I recommend libraries that supply flatMap for easier data manipulation.

  • @dimitro.cardellini
    @dimitro.cardellini 11 месяцев назад +5

    I've used Either and PromiseEither written by our team for two years.
    And it seems the Either concept couldn't be replaced with something else, because Either is needed to strictly define the contract for recoverable errors and left the exceptions mechanincs for the unrecoverable ones.
    But about Maybe -- ... Maybe is absolutelly sensless in modern Ecma- and Type- Scripts.
    Now we have ??, ??=, ?., ?.() and ?.[] -- it allows to write more consice and readable code.
    I will try to rewrite example from video with the same library but with my own understanding of the approach -- will share some later what I get.

  • @c4tubo
    @c4tubo 9 месяцев назад +1

    This code has really only included a few commons aspects of FP: the use of sum types for error handling (not actually exclusive to FP), the map(...) transform found in functors (FP), and a couple pure functions. The rest of the code shown is not FP. The main point missing is functions independent of state--the opposite of OOP. Most of the behaviour is not in the form of functions by rather as class methods bound to state. In FP functions should also be pure where possible, i.e. referentially transparent by only acting on values passed in and returning a result without depending or modifying the environment in any way. invert(...) and decode(t) are the only functions that look to be pure, all the others have side-effects. There is the passing of decode(t) and a lambda expression (call to invert(...)) being passed as arguments to .map(...), so that is so far the most FP aspect here. But there remains a lot of shared mutable state which is the main problem that FP strives to resolve, as OOP intends to resolve via encapsulation but fails to do so.

  • @feldinho
    @feldinho 11 месяцев назад +1

    In practice, functor is anything with a map-equivalent function. When passing a function, it will return another container of the same type with the processed data inside. Ex: Array.map(A => B) = Array; Array.map(A => Array) = Array
    Monads are anything with a flatMap-equivalent. When passing a function that returns the same container type, it will unwrap the contents of the result into itself. Ex: Array.map(A => B) = Array; Array.map(A => Array) = Array

  • @rak3shpai
    @rak3shpai 11 месяцев назад +7

    I had gone full functional a few years ago, but I've dialled it back quite a bit, and prefer idiomatic TS these days. Newer JS features like optional chaining, nullish coalescing, and TS's union types like `string | undefined` generally replace the maybe monad for me. Sure, maybes are even more composable, but idiomatic JS isn't too bad, and is idiomatic. Either just feels like a solution for languages that don't have try / catch. The years I spent deep-diving into FP has had a huge impact on how I write code, so I strongly recommend people do this as an exploration, but I would not go full-on FP in my everyday code.

    • @Hizbullla
      @Hizbullla 11 месяцев назад

      This is interesting. I'm a mid level engineer and I've been dabbling in the idea of learning FP hoping that I would impact the way I write and - more importantly - understand code. But I feel as if I need to full embrace FP for a while to actual get to that point, over just using it casually.

    • @jboss1073
      @jboss1073 11 месяцев назад

      @@Hizbullla Go full Haskell or don't bother with FP.

    • @OnurErenElibol
      @OnurErenElibol 10 месяцев назад

      I totally agree with you as the guy who passed the same ways.

  • @jboss1073
    @jboss1073 11 месяцев назад +4

    Please don't judge Functional Programming by how it is poorly rendered in Typescript which is a language with an unsound type system. Only judge Functional Programming languages in sound type system languages.

  • @zhypoh
    @zhypoh 11 месяцев назад +4

    I find with functional programming, it's important for readability to try to keep each function generally working around a single transformation. For example, if you change the role of the methods of the LocalStorageP to turning the key into `Promise` this allows you to simplify the `get` method quite a bit, since the type coming back is already a Maybe:
    ```
    const get = (key: string): EitherAsync => {
    const p = LocalStorageP.getItem(key)
    .then(Right, Left);
    return EitherAsync.fromPromise(() => p)
    .map(maybe => maybe.map(JSON.parse))
    .map(maybe => maybe.map(decode));
    }
    ```

    • @zhypoh
      @zhypoh 11 месяцев назад

      Thinking about this a bit more, it also probably makes sense to unwrap the Maybe when converting to the Either. It doesn't really make sense for the the value to be Nothing, but there to be no error message, so the type should be Either not Either. The get function for that would look something like this:
      ```
      const get = (key: string): EitherAsync => {
      const futureMaybeObj = () => (
      LocalStorageP.getItem(key)
      .then(maybe => maybe.toEither('No value found'), Left)
      .then(either => either.chain(parse))
      .then(either => either.map(decode))
      );
      return EitherAsync.fromPromise(futureMaybeObj);
      }
      ```

  • @alexjohnson-bassworship3150
    @alexjohnson-bassworship3150 11 месяцев назад

    Good stuff! I would say that, while trying to write code like this and catch as many runtime scenarios as possible before the code is actually run is a good practice, I still think the state of development in general still doesn't let itself well to this in many scenarios. This situation like this being one of those. I think setting up the code for successful error catching can still be more solid way of programming until there are tools that exist that make this kind of thing a lot easier. But the code you wrote, though, was sound logic, and definitely well thought through. Just a hard use case with tools that we have at our disposal.

  • @jackhere2023
    @jackhere2023 11 месяцев назад +1

    i think it only did one thing: convert logic code such as 'if else' into literal expression, do we should use '1+1' or 'one plus one' in math calculate?i think former is better

  • @sunny7268
    @sunny7268 11 месяцев назад

    is purify-ts is alternative to Ramda or it is just a lib of types to make your code functional?

  • @adambickford8720
    @adambickford8720 11 месяцев назад +1

    In general, you don't want to be explicitly dealing with the 'control flow' types this much. Having nested maybe/eithers also feels really off, you ideally want to 'collapse' those down as early in the chain if you have that issue. Its kind of like having Maps of Maps, it works but its going to hurt and not really how they are mant to be used in typesafe languages.
    Honestly, RxJs is hard to beat of you want async FP w/a lot of the heavy lifting built in. Instead of having all those wrappers it essentially creates monad (Publisher) that takes higher order functions for each 'path': "succes", "failure" and "done". By default, the error path will chain to the next operator if you don't pass in a h-o-f, so in practice you just have a chain of happy path and only specify a h-o-f if you want to recover, log, etc at each 'junction'. It REALLY helps to put each operation on its own line, especially for debugging

  • @officemax3977
    @officemax3977 11 месяцев назад

    I actually started following the channel back when you were toying around with ramda

  • @zxyi9090
    @zxyi9090 Месяц назад

    The way I think about FP is like a point free data pipeline. Also would recommend ramda or lodash

  • @joshdotmp4
    @joshdotmp4 11 месяцев назад

    To me, these functional tools and “rust-like” wrappers for types makes a lot of sense from a safety perspective assuming that typescript gives you the same guarantees with this library. The code seems messier because it’s handling more, and being more explicit. Other things in Rust that I feel like help this are its emphasis on the match statement, which is just another way to check that you’re handling all possible values, and it’s “if let” syntax for extracting a value out of a wrapped type. Would be interesting to know if there's anything equivalent with this library that can help catch errors at compile time. On your point about “Either” vs “Maybe”, I recently watched a video called “A Better Way to See Results” about how, in Rust, Option is just a specific subset of Result. I think that is the same case with this typescript library and may help inform your thinking about it!

    • @dupersuper1000
      @dupersuper1000 11 месяцев назад

      There are pattern-matching libraries in TypeScript, so you can get something similar to Rust’s match statement. But generally those libraries add some performance overhead, so it’s not great for hot paths.

  • @noriller
    @noriller 11 месяцев назад

    Is this for web or mobile? If its for web, I think localstorage is sync.
    As for the paradigm... its more verbose and you need to to know it before starting to understand whats going on, but even without knowing anything I can understand some of it. And it might be me, but breaking stuff in more lines help me understand better because you have to think "one thing" at a time.

  • @dimitro.cardellini
    @dimitro.cardellini 11 месяцев назад +2

    As promised:
    iimport { EitherAsync, Either, Right, Left } from "purify-ts";
    interface AsyncBrowserStorage {
    getItem(key: string): Promise;
    setItem(key: string, value: any): Promise;
    }
    class KeyNotFoundError {}
    interface PersistentStorage {
    get(key: string): EitherAsync;
    set(key: string, value: T): EitherAsync;
    }
    class BrowserStorage
    implements PersistentStorage {
    constructor(
    private readonly store: AsyncBrowserStorage,
    private readonly encode: (value: T) => Either,
    private readonly decode: (value: string) => Either,
    public prefix: string = ""
    ) {}
    private itemKey(key: string): string {
    return `${this.prefix}${key}`;
    }
    get(key: string) {
    return EitherAsync.fromPromise(() =>
    this.store
    .getItem(this.itemKey(key))
    .then((value) =>
    value != null ? Right(value) : Left(new KeyNotFoundError())
    )
    .then((result) => result.chain(this.decode))
    );
    }
    set(key: string, value: T) {
    return EitherAsync.liftEither(this.encode(value)).chain((result) =>
    this.store.setItem(this.itemKey(key), result).then(Right)
    );
    }
    }
    const localStorageAsync: AsyncBrowserStorage = {
    async getItem(key) {
    return localStorage.getItem(key);
    },
    async setItem(key, value) {
    return localStorage.setItem(key, value);
    }
    };
    class ObjectIsNotSerializableError {}
    const jsonEncoder = (value: unknown) => {
    try {
    return Right(JSON.stringify(value));
    } catch {
    return Left(new ObjectIsNotSerializableError());
    }
    };
    class InvalidJsonError {}
    const jsonDecoder = (value: string) => {
    try {
    return Right(JSON.parse(value) as unknown);
    } catch {
    return Left(new InvalidJsonError());
    }
    };
    const myStorage = new BrowserStorage(
    localStorageAsync,
    jsonEncoder,
    jsonDecoder,
    "json."
    );
    function assertNever(value: never, message: string): never {
    throw new TypeError(message);
    }
    async function main() {
    await myStorage.set("key", 12);
    myStorage.get("key2").caseOf({
    Left: (error) =>
    // prettier-ignore
    error instanceof KeyNotFoundError
    ? console.log("Key is not found") :
    error instanceof InvalidJsonError
    ? console.log("JSON parsing failed") :
    assertNever(error, `Unexpected Error ${error}`),
    Right: console.log.bind(null, "Received")
    });
    }
    main();

  • @feldinho
    @feldinho 11 месяцев назад

    Fun fact: Promise is an inverted "Either". Results go left, errors go right. By passing a function to the second parameter of `then`, you can treat the error and return a value to send it left (result) or throw another error to keep it right.

    • @feldinho
      @feldinho 11 месяцев назад +1

      (I have no idea if what I said makes much sense. english is not my first language and I find it hard talking about code without using code blocks or examples)

  • @arnabthakuria2243
    @arnabthakuria2243 11 месяцев назад

    hi can anyone tell me what is the name of the font he uses

  • @HappyCheeryChap
    @HappyCheeryChap 11 месяцев назад

    Seeing you're asking for feedback on code here, you should post a link to it on pastebin or or the typescript playground something. So that we can easily read it in full.

    • @andrew-burgess
      @andrew-burgess  11 месяцев назад

      Good point! I've added a link to the code in the video description.

  • @les2997
    @les2997 11 месяцев назад +3

    Maybe, Array and Promise are monads. I'm wondering myself how useful this type or programming is to JavaScript devs.

    • @zhypoh
      @zhypoh 11 месяцев назад

      Maybe, Array and Promise aren't all monads. Only Maybe is a proper monad, Promise are almost monads (but not quite), and Arrays are semigroups.
      For the curious, Promise aren't generally considered monads because:
      1) They execute as soon as they are created (have side-effects)
      2) "then" acts differently depending on the result of the handler. If it's a promise, it's automatically chained (Promise becomes just Promise), otherwise it's treated like a map. A monand wouldn't do this: map would map, chain would chain.

    • @dimitro.cardellini
      @dimitro.cardellini 11 месяцев назад

      ​@@zhypoh that are not monads why?
      Monad is a mathematical abstraction applied in programming to generalilze composition of computations with different effects.
      Each time when we write code without writing if to check computation effect status -- we use some monad (it means there is at least one monad that could be defined on this computations). Even x?.y?.z -- is example of monad application.
      So, Monad -- is not a burito, is not a box, and it is not something implementing Haskel Type Classes, Fantasy land or FP-TS specification or something else.
      Finally, each time we say "Promise" is not a monad, we should add "from Haskel/Fantasy Land/whatever point of view".

    • @zhypoh
      @zhypoh 11 месяцев назад

      The usefulness of having all these explicit types is a bit dubious. You can get 99% of the benefit just using Promises.
      I think when you have functions that can return null or some value, wrapping it in a some types of Monad-ish thing does make for less bug prone code, since it forces you to check for the error state before accessing the value, preventing the oh-so-common "Cannot read properties of null ".
      There is value about learning about these functors, and functional programming concepts in general. But, trying to do pure-functional programming in JS is generally not worth it. The true value comes from knowing how to use the stuff that's in already in JS better. Like, for example, chaining promises. A lot of devs don't really think about the fact that .then ALWAYS returns a promise; so rather than chaining a few .then together, they just create one-big "handler" function. Nested functions have always been one of JS biggest issues, and reducing it will make for more readable code the vast majority of the time.
      There is also little readability things you don't really think about unless you've dabbled in FP. Like the fact that `.then(x => foo(x))` is silly and redundant; just do `.then(foo)`.
      Ultimately the benefit you'll get out of this stuff has a lot to with how you think about problems. All programming styles have their strength and weaknesses, so use the styles that make sense to you, and try to just understand them well.

    • @dimitro.cardellini
      @dimitro.cardellini 11 месяцев назад

      ​@@zhypoh hey, amico ...
      it seems your comment must be in other place )
      Actually I agree that we should use native ES/TS staff for maximum and bring only stuff absent in the languages.
      bu the way, .then(x => foo(x)) often is very rare case ... often the code looks like: .then(x => foo(x, bar)) or .then(x => bar.foo(x)).
      So, it depends ... But yes, it is good idea to have functions prepared to be used in chains and be possible to write:
      .then(foo(bar)) or .then(bar.foo), instead of two previous samples.

    • @zhypoh
      @zhypoh 11 месяцев назад

      @@dimitro.cardellini Ok. A promise is not a monad from the point of view of math.

  • @deatho0ne587
    @deatho0ne587 11 месяцев назад

    I sort of agree with what you are saying at the end everytime I see functional code it sort of loses its meaning. Also not the biggest fan of using packages like this either, next month/year the code could hit a bug due to this package. Then your team might not really understand what is going on and be an issue with this package. A lot easier to mess around with local libraries than it is with external ones.

    • @jboss1073
      @jboss1073 11 месяцев назад

      Build your own Maybe or Either if you don't trust libraries. It's easy enough.

    • @deatho0ne587
      @deatho0ne587 11 месяцев назад

      It is not that I do not trust libraries.
      It is that sometimes people that add packages that are from 2013 that barely worked then, that could be done in 2017 with basic JS as long as IE did not have to be supported.
      FYI: This happened this year.

  • @br3adina7or
    @br3adina7or 11 месяцев назад

    tbh i think just replicating functional-paradigm monads in ts isn't a great choice, except in certain cases (probably custom-made, not using a lib). ultimately typescript's type system is just (effective) mass delusion and that carries a lot of sticking points for trying to force it to be something it isn't. without direct language support from the get-go there'll always be innumerable hurdles.

  • @antonmas3451
    @antonmas3451 11 месяцев назад

    fp-ts library doing the same? and you hint that there is no point of using FP in frontend prod? it's too difficult to maintain , to understand to write etc. and you suggest to go back to friendly OOP paradigm
    am i get you right?

  • @Luxcium
    @Luxcium 11 месяцев назад

    You Rock 💥🪨🔥Thanks 🙏🏼 for addressing this topic!!!

  • @nulljeroka
    @nulljeroka 11 месяцев назад +2

    I find redability to be a mess. And you did it right. Which is why FP has not been widely adopted

  • @CelesteOnYoutube
    @CelesteOnYoutube 11 месяцев назад

    I think it's either a good idea or not ^_^

  • @vukkulvar9769
    @vukkulvar9769 11 месяцев назад

    You should compare the performances too. It's quite easy to have several loops in FP that would be a single one otherwise.

  • @ThiagoMarquesdeOliveira
    @ThiagoMarquesdeOliveira 11 месяцев назад

    Go ahead and study Monads. I started to look at Promises with better eyes now.

  • @user-fed-yum
    @user-fed-yum 11 месяцев назад +4

    Over the years I've seen many an architect astronaut construct such unmaintainable, overly complex, and unnecessary theoretical rigor. Even if it looked like a good thing on their phd. It never gets released.

    • @jboss1073
      @jboss1073 11 месяцев назад

      You must be joking. The astronauts are the OOP programmers. Maybe and Either are not astronautics, they are fixes to the billion dollar mistake, the null value.

  • @BrockFredin
    @BrockFredin 11 месяцев назад

    The beauty of functional programming is reducing complexity by using only a few operations for most objects. I'm not sure this approach is consistent with that idea.

    • @jboss1073
      @jboss1073 11 месяцев назад

      Typescript itself is not consistent with the idea of Functional Programming due to lacking a sound type system which is a sine qua non for FP.

  • @dimitrimitropoulos
    @dimitrimitropoulos 11 месяцев назад +2

    immediate up vote for anything promoting FP

  • @voidmind
    @voidmind 11 месяцев назад

    There is a total lack of people who can explain algebraic data types with a terminology that makes sense to the average programmer (especially the non math inclined). If a Monad can be explained with the concept of a wrapper, then by all means use that term.
    BTW, algebraic data types are part of functional programming, but it's not correct to call it functional programming, which is a lot more than data types.

  • @thevector
    @thevector 11 месяцев назад

    No one seems to be saying this directly... I write fp in ts daily using fp-ts. Your sample project is not idiomatic fp and I def. would not use fp if it could not be easier to read than this (but my first attempts were far worse, I promise, not trying to be rude here). The `fromPromise.map` statement does not make any sense to me and having to build that `invert` function is... yeah you should not need something like that(not idiomatic fp). Without having the code to work with outside the video it is hard to say, but what you have does not make sense when I look at the purify-ts docs for EitherAsync. I am pretty sure there is a cleaner way to do that bit of logic. (Post the code somewhere if you want to get into the details.)
    Obv. I am biased by using a diff. library, but I don't love the purify-ts API. I am not sure it is the right lib to base your opinion of fp in ts. (Purify-ts authors, hey, I am sure you know what you are doing and we could debate it quite a bit, but I am just giving my current honest opinion to the video creator.).
    But yeah, fp-ts _is_ huge and there is no super easy place to start. It is currently merging with the effect-ts library which aims to be more approachable (YMMV, I have yet to use it). Either way, learning FP is just not a small undertaking to be honest. But I am happy to contribute to playing around with your example online somewhere though, as a way to explore the idea, if you were interested.

    • @andrew-burgess
      @andrew-burgess  11 месяцев назад

      Thanks for your comment! I added a link to the code in the video description, if you take a look, I'd love to hear your thoughts.

  • @elmalleable
    @elmalleable 11 месяцев назад

    If you want to log stuff and debug meaningfully youll need blocked anonymous functions.
    For readability just assign the oneline functions to descriptively named variables
    Pick your poison

  • @david.baierl
    @david.baierl 11 месяцев назад +1

    I don't get it. Why not simply use a null return. so return T | null. or undefined if you are a fan of it.
    but Maybe feels more like, yeah possible undefined.. or Promise in case of async. I mean it feels like adding more complexity only to show how smart you are.
    I don't see any benefit over a simple union type return.
    and for your map purpose: simply create a new pure function with some early returns in case of undefined. and then simply call them before returning.
    It makes it really hard for new developers to learn this. it doesn't help writing less code nor makes it simpler to understand.
    so: no we are not functional programming code like that in our company. We mostly use guards and early return and try to lower the complexity of a function.
    your local storage "get" could be far easier to read if you simply make it async and write an if statement after another for determining what to return.
    and for sure will it be a little more faster if you don't wrap anything inside thies helpers, making debuging a bit easier removing this noise from error stacks

    • @feldinho
      @feldinho 11 месяцев назад +1

      I agree with you. T|null already is an algebraic data type, since it's using the union operator.
      I see the value of using Maybes and Justs when dealing with pattern matching like Haskell, but TS is not Haskell and that's okay. There's no much sense writing Haskell in JS. I think A better functional paradigm for JS would be Ramda or lodash/fp, since those don't try to be a different language altogether.
      Also, TS don't use nominal data types, where the type can be a value by itself; so "Nothing" is just a "null" with extra steps.

    • @jboss1073
      @jboss1073 11 месяцев назад

      @@feldinho "Also, TS don't use nominal data types, where the type can be a value by itself; so "Nothing" is just a "null" with extra steps."
      1. That is not what "nominal types" means. C has nominal types and types cannot be values by themselves.
      2. Nothing is never "null with extra steps". Nothing is technically a tagged discriminated union. Comparatively, the null value is not tagged, and so code meant for the non-null values can easily accidentally use the error value on their sunny-day path.

  • @Lebastian
    @Lebastian 11 месяцев назад

    you are looking for unknown errors and that's not the right thing to do, if something is an expected error it should not be considered as error

  • @MindlessTurtle
    @MindlessTurtle 11 месяцев назад

    undefined, unknown, null -- typescript has too many redundant types / states for nothing

    • @ThiagoMarquesdeOliveira
      @ThiagoMarquesdeOliveira 11 месяцев назад

      Unknown?

    • @feldinho
      @feldinho 11 месяцев назад +1

      while undefined and null are relics from js, unknown is just an annotation to tell TS to make no assumptions about the data and require the code to validate it on runtime. Those are completely different beasts.

  • @kilo.ironblossom
    @kilo.ironblossom 11 месяцев назад +1

    The whole overdosed declarative functional programming is just missing the basic design principle, KISS.

    • @jboss1073
      @jboss1073 11 месяцев назад

      What is simpler then being explicit about what the types are? That is how the compiler is able to do so much type inference - because you've made the code so simple as a functional programmer that even a dumb computer can understand. On the other hand, the dumb computer cannot understand your OOP code before running it. That's because it is too complex and not KISS. Understand now?

    • @kilo.ironblossom
      @kilo.ironblossom 11 месяцев назад

      @@jboss1073 When it comes to functional vs imperative programming it's readability what matters. It has nothing to do with compilation. And the TS compiler understands both OOP and Functional approach and in the end emits JS. I think you are far away from the point that I made by using the word "overdosed".

    • @jboss1073
      @jboss1073 11 месяцев назад

      @@kilo.ironblossom TS does not understand FP as it does not have a sound type system. FP cannot be had in unsound type system programming languages. FP is not "a design pattern" the way OOP programmers think of them. FP is about guarantees, and no OOP language that is type-unsound can give me them. So only in sound-type languages can FP be had.

    • @jboss1073
      @jboss1073 11 месяцев назад

      @@kilo.ironblossom @kilo.ironblossom TS does not understand FP as it does not have a sound type system. FP cannot be had in unsound type system programming languages. FP is not "a design pattern" the way OOP programmers think of them. FP is about guarantees, and no OOP language that is type-unsound can give me them. So only in sound-type languages can FP be had.

    • @kilo.ironblossom
      @kilo.ironblossom 11 месяцев назад

      @@jboss1073 And again you are going further from the point. On top of that your point sounds very vague.
      First, I have never mentioned 'pattern' but 'principle' like DRY and it's KISS not FP. Just in case you don't know the difference here's a quick Google :
      _principle is an abstraction, a guide to design. A pattern is an implementation that solves a particular problem_

  • @markokraljevic1590
    @markokraljevic1590 11 месяцев назад

    you just included exception in return type, nothing to gain here and code looks overly complicated