4 ms·
I didn't think much of this article ("Another biased comparison, yawn") until I got to the Reducer example. I read up on reducers for Redux and Cycle.js, but a
by pbowyer 7y ago
I didn't think much of this article ("Another biased comparison, yawn") until I got to the Reducer example.
I read up on reducers for Redux and Cycle.js, but avoided using them. The Typescript examples are so cumbersome; I'd rather do something else where my code is more understandable.
But in ReasonML it's elegant. It's awesome! I'd choose to use that.
Sure they do the same thing. But as the developer who has to commit to reading and maintaining this code, I know which I'd rather write and come back to in 18 months' time.
- quickthrower2 7y agoI wonder if people (including me) are too afraid to write a little DSL/compiler to get us out of these spots. I.e. use Typescript most of the time, with a DSL for things like reducers. Another thing where a DSL/compiler step would be good is to convert a type into a function that will validate arbitrary JSON (e.g. received on the wire) conforms to the type.
- rkangel 7y agoThe reducer is a case where the functional programming concepts like sum types make a dramatic difference, but they are widely useful across your codebase. It shouldn't be a surprise that React works well in ReasonML - the first prototype of ReactJS was written in StandardML before the creator ported it to JS (https://reasonml.github.io/docs/en/what-and-why.html https://reasonml.github.io/docs/en/what-and-why.html)
- undecidabot 7y agoLooks like another biased comparison to me. You could easily rewrite the TypeScript reducer example (which fails to compile btw) to be similarly concise while being as safe. interface State { movies: string[] } // you may also use { tag: "Action", payload: string } // if you prefer something more structured type Action = | ["AddMovie", string] | ["RemoveMovie", string] | ["Reset"] const defaultState: State = { movies: [] } const reducer = (state: State, action: Action): State => { switch (action[0]) { case "AddMovie": return { movies: [action[1], ...state.movies] } case "RemoveMovie": return { movies: state.movies.filter(m => m !== action[1]) } case "Reset": return defaultState } } const someAction: Action = ["AddMovie", "The End of Evangelion"] TypesScript's type system is actually very flexible and advanced [1] compared to most type systems since it had to adapt to the very "dynamic" structuring of JavaScript in the wild. [1] https://www.typescriptlang.org/docs/handbook/advanced-types.html https://www.typescriptlang.org/docs/handbook/advanced-types....
- hajile 7y agoReasonML variants are type constructors. Your example completely eliminated all action creators (constructors). Once you add those functions back, most of the type bloat reappears. I wouldn't want to work on a project where everyone was creating object literals and passing them to dispatch everywhere.
- mikewhy 7y agoThis is what my redux stuff looks like with TypeScript: const constants = { addMovie: 'movies/addMovie', } export const addMovie = createAction<string>(constants.addMovie) export const reducer = createReducer(initialState, { [constants.addMovie]: (state, action: ReturnType<typeof addMovie>) => ({ ...state, }) }) But this forgoes applying any constraints to `dispatch` / the ability to have one giant union of all possible action types your app supports.
- undecidabot 7y agoYes, I eliminated the action creators. I don't see why they're necessary (maybe there's something about redux I'm missing?). The type constructors of ReasonML cannot be used as functions, so I think my rewrite is fair. If you insist on having the action creators, they can easily be written as one liners each, which is a lot less bloated than the original example. export const addMovie = (movie: string): Action => ({ tag: "AddMovie", movie }); What's the issue with passing object literals? I know it looks a bit hacky and messy, but if it's typesafe (which it is) then it doesn't seem like a real issue.
- hajile 7y agoNo action creator is a serious refactor concern. Let's say you need to update your function signature with an additional, optional parameter with a default value. You can go to the definition of the action creator and change it. Since you've defined an interface for the action object, finding reducers that use it will be reliable and easy. If such a creator doesn't exist, you must grep your codebase for every instance of the object and ensure they deal with the optional bit. Murphy's Law guarantees that someone will have decided to procedurally generate the object making finding every instance very hard. Next, you have that reducer lookup, but you don't have a shared interface, so you're stuck in grep land once again to find all the reducers. Finally, copy-pasting an object literal all over generally results in a larger bundle than reusing an action creator which is smaller and also minifies better.