Class-based Tree Shaking - HTTP203
ฝัง
- เผยแพร่เมื่อ 6 ก.ย. 2024
- Jake Archibald and Surma talk tree shaking in this episode of HTTP203. And show you some code! For the first time. Lovely.
Subscribe to Chrome Developers here: goo.gl/LLLNvf
Watch more HTTP203: goo.gl/bTQMrY
Listen to the HTTP203 podcast for a full hour conversation & more: goo.gl/LR7gNg
Itunes: goo.gl/cf2yRq
It absoluteley is possible for typescript, here you go:
type ParamsOf = T extends (...args: infer P) => any
? P
: never
type ReturnOf = T extends (...args: any[]) => infer R
? R
: never
class DoClass {
do(func: T, ...args: ParamsOf): ReturnOf {
return func.call(this, ...args)
}
}
const x = new DoClass()
function hallo(a: string, b: number, c: Error) {}
x.do(hallo, '', 2, new Error())
As of TypeScript 3.0, it is now possible to do what you want using the new and improved tuple features (albeit without JSDoc and autocomplete).
Great video! Certainly a great improvement to show code :)
You can do that in TypeScript now.
function do(doFn: (...args: A) => R, ...args: A): R { return doFn(...args); }
works nicely, sadly argument names are missing though
I'd use it in a pet project anyway
@@amatiasq Yes, that's the only real problem with it now. It would be a nice little improvement from the TypeScript team if they were to implement it.
@@amatiasq `do any >(func: T, ...args: Parameters): ReturnType` works with argument names as well.
Yes, thanks for listening to the feedback, great video!
Well, yes, the code samples are a great addition, but where can we get the Downasaur and awkward family portrait coasters? 🤔
That pipe operator is a beautiful beautiful thing in Elm and languages alike. It also goes hand-in-hand with partial application which is kinda covered in JS with `bind`. Trust me fellas, you would very much enjoy it if you practice even a little bit of functional style transformations.
Oh we do enjoy it, but the problem is that functional programming languages are very much designed to create one-off functions on the fly while in JavaScript those create actual garbage that needs to be collected. Syntactically I _love_ pipeline, but as we outline in the video, it’s really awkward to create functions that return functions and it’s a legit performance concern.
how big of a performance concern?
I thought that this video neatly shown how ugly (in JavaScript context) it is ;-)
Nevertheless let's bring ::bind! it's pure, simple, readable, and very very useful.
One of the many reasons why I like Haxe over TypeScript.
Haxe has had Static Extensions since forever. (the "using" keyword)
Hey guys! Just want to drop an example. It is possible to do the 'do' function in typescript now.
const kRecords = Symbol('records')
export type StoreFunction = (...args: any) => any
export class Store {
public [kRecords]: Record
constructor() {
this[kRecords] = {}
}
do(fn: S, ...args: Parameters): ReturnType {
return fn.apply(this, args)
}
}
export function get(this: Store, key: string) {
return this[kRecords][key]
}
export function set(this: Store, key: string, value: T) {
this[kRecords][key] = value
}
// Meant to be in another file
const main = () => {
const store = new Store()
store.do(set, 'Hello', 'world!')
store.do(get, 'Hello')
}
if (true) {
main()
}
I don't see why can't the set function receive 3 arguments... it's easier to grasp than almost all the alternatives.
Other than that, partial application is a beautiful thing once you get the hang of it! Right now it's not the more performant way but I'm sure the engines will optimize them in no time!
Here is the TS implementation:
```ts
function do(this: TThisType, f: (this: TThisType, ...args: TArgs) => TReturnType, ...args: TArgs) {
return f.call(this, ...args);
}
```
You would use it like this:
```ts
function set(this: Store, key: string, value: string) {
this._innerDb.set(key, value);
}
class Store {
_innerDb = LocalStorage;
do = do;
}
const store = new Store();
store.do(set, "foo", "bar");
```
The only (but IMHO significant) downside to this is that members of Store cannot be private anymore. You would essentially be stuck with classes which behave like structs in C.
@10:40 TypeScript: "It's definitely more complicated than it should be" 🤣
It can be done in TS 2.8, but you would need to wrap the args inside an array
type ArgsOf any> =
T extends (this: any) => any ? never :
T extends (this: any, a1: infer A1) => any ? [A1] :
T extends (this: any, a1: infer A1, a2: infer A2) => any ? [A1, A2] :
T extends (this: any, a1: infer A1, a2: infer A2, a3: infer A3) => any ? [A1, A2, A3] :
T extends (this: any, a1: infer A1, a2: infer A2, a3: infer A3, a4: infer A4) => any ? [A1, A2, A3, A4] :
// create overrides up to A10 for example
any[];
class Store {
do any>(func: F): ReturnType; // no args
do any>(func: F, args: ArgsOf): ReturnType; // args in an array
do(func: (this: Store, ...args: any[]) => R, ...args: any[]): R { // base generic case
return func.call(this, ...args);
}
}
function set(this: Store, x: number, y: string) {
// do something
}
function clear(this: Store) {
// do something
}
function anotherClassSet(this: string, x: number, y: string) {
// do something
}
const store = new Store();
store.do(set, [ 'ho', 'hi' ]); // won't compile, invalid arg type
store.do(set, [ 5 ]); // won't compile, too few args
store.do(anotherClassSet, [ 5, 'hi' ]); // won't compile, not a method for the Store class
store.do(set, [ 5, 'hi', 8 ]); // won't compile, too many args
store.do(set); // won't compile
store.do(clear, []); // won't compile
store.do(set, [ 5, 'hi' ]); // ok!
store.do(clear); // ok!
Assuming TS ever adds support for ...args: [number, string] or some such then it would be possible without the array wrapping
Thanks for the detailed example!
Another try, works as intended (the params do not need to be wrapped in an array), but it requires to add the possible methods that can be invoked to the "do" signature
type FuncSignature =
T extends (this: any) => infer R ? (fn: T) => R :
T extends (this: any, a1: infer A1) => infer R ? (fn: T, a1: A1) => R :
T extends (this: any, a1: infer A1, a2: infer A2) => infer R ? (fn: T, a1: A1, a2: A2) => R :
T extends (this: any, a1: infer A1, a2: infer A2, a3: infer A3) => infer R ? (fn: T, a1: A1, a2: A2, a3: A3) => R :
T extends (this: any, a1: infer A1, a2: infer A2, a3: infer A3, a4: infer A4) => infer R ? (fn: T, a1: A1, a2: A2, a3: A3, a4: A4) => R :
// add more...
T extends (this: any, ...args: any[]) => infer R ? (fn: T, ...args: any[]) => R :
never;
class Store {
do: FuncSignature & FuncSignature = (func: Function, ...args: any[]) => {
return func.call(this, ...args);
};
}
function set(this: Store, x: number, y: string) {
// do something
}
function clear(this: Store) {
// do something
}
function anotherClassSet(this: string, x: number, y: string) {
// do something
}
const store = new Store();
store.do(set, 'ho', 'hi'); // won't compile, invalid arg type
store.do(set, 5); // won't compile, too few args
store.do(anotherClassSet, 5, 'hi'); // won't compile, not a method for the Store class
store.do(set, 5, 'hi', 8); // won't compile, too many args
store.do(set); // won't compile
store.do(clear, []); // won't compile
store.do(set, 5, 'hi'); // ok!
store.do(clear); // ok!
This gets even more complicated if any of the "methods" have a generic element to them
Yeah, but who would do _that_? Come on.
The Store.do method is now possible in TypeScript!
yes, it is very understandable than before, please include code example in all sessions.
Thank you sooo much, i have been scratching my head around this for around 3 days now, but honestly Store.do is what i liked the most, unfortunately will have to opt out the types 🙃
The do() method makes "static" calls into "runtime" calls, it makes call stack unnecessarily polluted, and render autocomplete and jsdocs useless...
victornpb In the age of Visual Studio Code, saying "can't be defined in TypeScript" is the same as saying "breaks autocomplete".
Which is why I want the bind operator.
What if, instead of a prototype.do method, you have a prototype.add method. Ex:
import { get, Store } from './keyval.js';
const theStore = new Store();
theStore.add(get);
theStore.get('hello');
// It's like "do it yourself" classes.
// prototype.add could be:
add(method) {
let name = method.name; // functions have a .name property
this[name] = method;
}
I wonder if we couldn't get the static analyser to detect if any square bracket operators are used on the Store object. If there are, the Store is marked as a dirty object and can't be tree shaken. If there aren't then it can be tree shaken.
This would probably solve 90% of the tree shake issues. And the remaining 10% can use the more obscure solutions you mentioned in the videos.
I quess it would still be an issue for certain data type classes. For instance a List class that has a shift method, but also an index operator to get a specific item. Although you could replace the List[index] with List.get(index)
Bit late to the party. But how about
import { get, set, Store } from './idb-keyval.js';
const store = Object.assign(new Store('my-store'), { get, set });
well you'll have to do that for each instance and it's up to the devs to include only the methods that they are going to use
Thanks for code examples. A real nice addition to your series.
Something Wonderful just started in this series.
There is a proposal for Variadic Kinds on the TypeScript Github (#5453) that would allow rest parameters to be defined as tuples, that (maybe with a little help from the new conditional types in TS 2.8) would make this possible.
Its theoretically possible right now with an arbitrary amount of overloads, but that would be super ugly.
Another option would be `store.do(set)("hello", "world")`, as `store.do(set)` would have the exact same signature as `set` itself.
It can be achieved with overloads and be ugly, but who cares if it's being imported from a module/library that you as a user don't see it's code?
Great video!
You could also solve the reversed order of nested arguments: h(g(f())) with a pipe function (not operator) that accepts 'pipeable functions': pipe(f(), g(), h()), like it is done in RxJS or callbags.
This can be strongly typed without Variadic Types. see following code sample:
You can copy this to the 'Typescript playground' and change the order of the pipeable functions and you will get errors.
/** describes a function with one argument */
type TFn = (arg: TIn) => TOut
/**
* Pipe function as alternative of pipe operator: |>
* Pipe accepts pipeable functions
* A pipeable function is a function that returns a function that only accepts 1 argument (= the result of the previous function)
*/
function pipe(startValue: TIn, fn1: TFn): TOut;
function pipe(startValue: TIn, fn1: TFn, fn2: TFn): TOut;
function pipe(startValue: TIn, fn1: TFn, fn2: TFn, fn3: TFn): TOut;
function pipe(startValue: TIn, fn1: TFn, fn2: TFn, fn3: TFn, fn4: TFn): TOut;
function pipe (startValue: any, ...fs: Function[]) {
return fs.reduce((prev, current) => current(prev), startValue);
}
// Some sample pipeable functions that change types
const parseNumber = () => (s: string) => parseFloat(s); // string -> number
const plus = (valueToAdd:number) => (num: number) => num + valueToAdd; // number -> number
const formatCurrency = (prefix: string, suffix = "") => (valuta: number) => `${prefix}${valuta}${suffix}`; // number -> string
// A pipe example
const result = pipe(
"1", // "1"
parseNumber(), // 1
plus(2), // 3
formatCurrency("€"), // "€3"
)
alert(result); // €3
Partial application from the first version of |> is great but when would someone choose to use the second version to write a |> b(?,c) instead of just b(a,c)?
What does your last syntax mean? The `?` denotes what parameter the left-hand side should be used as.
Sorry, I meant the last question mark as an actual question mark, not part of the syntax. I'm asking when someone would use
a |> b(?, c)
instead of just
b(a, c)
Does the |> operator do anything other than just plug the left hand side into the question mark?
No, but it’s much clearer to read if you write pipelines and allows you to re-use existing functions where you don’t necessarily have control over where the argument is. E.g: `canvas |> turnToStream() |> compress(?, 'gzip') |> copyToServer('myServer.com', ?) |> putIntoCache('my-cache', ?)`
OK, that's what I wasn't seeing. Like Clojure's threading macros
Yes please! b(a,c) looks like a great solution for the video's example.
This is much better than before.
Here I thought you were going to talk about how Rangers can take a feat to scare birds out of their nests...
I ran into this very problem in isomorphic-git. At one point I had a constructor `let repo = new Git({fs, dir})` and instance methods... but after realizing some of the methods added _serious_ weight (100kb?!) I was determined to allow users to only import the methods they used. Ultimately I ended up with an API where the state management is external to the library. Instead of providing a constructor, I just provide all the operations as functions and make the user pass certain required arguments that I would have stored on "this" every call. `let result = fetch({fs, dir, ...args})`
Jake is a legend, love that dude
what's ugly about closures and curried functions? @8:15
You guys are awesome ! How can we buy those coasters ?
Classes in JavaScript aren't real, they're just objects with functions on prototype with their calling context bound to the instance of the object.
That said, I'd argue not using classes at all, especially if you're using typescript. Instead just use modules and namespaces.
I wish there was a way to simply import a whole class or object, like Jquery and have it be tree shaken.
Loved the video, this longs talks are great!
Great talk guys, did you manage to make .do approach work with typescript though?
Wouldn't it be easier to just make the methods static and pass in an instance? Something like:
let store = new Store();
Store.set(store, "hello", "world");
And the set method would be:
static set(instance, key, val) {
return instance._underlyingDB.setKeyVal(key, val);
}
Or, replicating the paradigm for the database:
static set(instance, key, val) {
return UnderlyingDB.setKeyVal(instance._underlyingDB, key, val);
}
No new syntax needed. Build tools could probably convert most regular code into this style of code automatically, as long as you're not calling methods by strings.
Static methods don't tend to tree-shake. What I went for in the end was set(key, val, { store }).
Why not use mixins to extend the Store class with whatever function is needed?
AdaLiszk TypeScript supports mixins, so that could work!
@William Hilton: I know, but in general mixins are part of the ECMAScript and a lot of other tools like Polymer uses it to extend its functionalities and have the tree-shaking work ;)
How do you use mixins with instances? Or am I misunderstanding you?
Mixins that only exist in a particular scope would be interesting, but pretty complicated. It feels like bind is just simpler.
@Surma: I would create my own class for my own needs using the building blocks provided the library then after I'm done my application I can remove the mixins one-by-one and ran the test to see which method is used in which cases. For reflecting the instance I can override the inner states to be constants in my own class, that way I can extend that even further in parts of the system where it actially needed.
Based on a tree story!
Great video , Keep it up 👍
hey... great vid !!!
Just to know... Can Typescript tree-shake class ?
As explained in the video: There’s no fool-proof way to prove that some dynamic code doesn’t invoke a method,
If you are going to change your classes to externalize the methods outside the class, and since JS classes can't natively support private data anyway, what's the point of using classes at all? Why not define your program in terms of data structures made of objects, and bare functions that operate on those structures, either purely or with mutations? Is there a way for a class to define what functions the pipeline operator (or the now-dead bind operator) can add, or can literally any code inject a method into a class? If the latter, you don't even have legitimate behavioral separation, so why pretend you do by using classes? Certainly, a `do` method on a class will remove all potential data integrity.
JavaScript is dynamically typed. Using that against a proposal seems not very useful (you have always been able to pass any object to any function). What’s the point of classes? Grouping data and the functions that operate on that data. You can invoke the method of one class instance on a completely unrelated class instance right now, so that’s not anything new. These 2 proposal just make it more obvious that _that_ is what’s actually happening. It’s not about was is allowed in the language, but about what conveys the intention of the developer most intuitively to another reader.
Just as you can pass any object to any function, you can also pass any object to any method on a class, right? I don't see how this is an argument against using plain objects and functions. Classes share the exact same potential vulnerability.
I understand that classes provide a way to group behavior and data together. But if grouping related data and behavior together is the focus, why not just create a plain object that contains your data and references (via keys) to functions you want related to the data? The only difference, functionally, is that you explicitly pass the object to the function, rather than implicitly relying on the user to use `new` and `this` properly. Both this system and class-based systems provide neither data privacy nor method security, so they are equivalent security-wise. From my perspective, the idea of splitting the method behavior outside of the classes at all (via pipeline or bind) seems to be itself a violation of the intention of classes, because the method is actually just a function that anything could potentially use for themselves, rather than something that is strictly for that class.
My proposal is to dispense with the subterfuge: functions that operate on objects correctly represent the security available to the API (that is, none) to the end user without wrapping it up in classes which make the code appear to have a strict API when it actually doesn't.
10:40 Why isn't it possible with typescript? Could it just be the "any" type?
The 'any' type is pretty much an opt-out of typescript when used in this way.
Thanks for video and code(finally :)).
I understand the problems it will resolve. However I would like raise the concerns that Dwight House have. And add that there is proposal for private class methods, however in my opinion it useless. Because by this video you need to treat all methods as a private. So maybe old fashion function and object are more suitable?
To be honest, I don't feel the `set` function that takes a function and returns a function used with pipe operator is ugly.
No, as I said, the ugliness is hidden. But if you have to _develop_ the functions that are supposed to be used with |>, it gets ugly (and produces a bit of memory garbage that :: avoids).
But Google offered its own copy of Jquery on its CDN so it would be cached for all but the first of the sites that use that, and on the Jquery site itself you can make a lib with only the functions you want. Ah, it's about variable scope... and horrible syntax that reminds me of Lisp, Perl, and other languages most people avoid due to lack of readability.
@suma Love the coster with you both on. Can I have one? :)
Please do a video about how good and important it is refactor code and keep your dependencies up to date.. I have a hard time convincing my managers everywhere I work, they don't see a "business value", but get anxiety attacks about things breaking everytime I want to change something, refactor some code to follow a best practice, improve performance, or update/migrate to the latest versions of dependencies
This should be possible now in Typescript 3.0 with genreric tuple spread/rest www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html
9:10 "…in the pipeline" 😏
Thx for the code :)
I do not like .do operator because it means it will privatize it... the pipeline operator is so ugly in the implementation, too complicated, not as straight forward as the bind operator. I do not see any trace of the bind operator in the TC39 github proposal repo. Any clues?
Guys, please add subtitles. Auto subtitles very bad and I much more worse in English.
We always have our videos captioned. It might take a day or two for those to be finished, tho.
No... What? Pipeline operator is beautiful and opens up a new world of functional programming (partial application, chaining of methods/promises).
Like your usecase isn't even the idomatic usage of pipeline operator, yet it could help you there. It's more versatile.
You are more likely to keep a function on RHS than a higher order function, it's just the RxJS community that loves functional programming a lot, and already has its codebase written in a way to support it. You can always rename original set function as `createSetter` and store its result in a `set` variable to make it less convoluted.
I agree that bind operator is elegant in this use case, coz set method was already using `this`, which can itself be ugly, but if you're going to dissect out all methods of class as functions, you can probably consider functional programming paradigm altogether.
Also, the other common usecase for bind operator would've been in case of React components. I feel public class fields solve it in a much better way and are already baked-in under a flag, so that kind of makes bind operator less useful. For all other cases (which I feel would be rare), using call/apply wouldn't hurt.
Sorry for being super opinionated about this.
Loved the code examples and the topic as whole. `do`approach seems really nice!
How about creating different classes and extending Store with it? Basically using inheritance to extend features of base class? E.g. BasicStore & EnhancedStore are exported by idb-keyval, user may use either based on appetite.
That’s what we have been discussing in the spec issue as well. I agree - this tree-shaking problem is not idiomatic for pipeline operator. The problem is that currently _either_ pipeline _or_ bind will happen, not both. This might change, but that’s the premise we used in this video.
With that in mind, I think the problems `::` solves are currently much more common than the problems `|>` solves. You _can_ shoe-horn the problems space of one operator onto the other, but then weird things happen, as we outline in the video.
If only there could be both...
Also I agree; the this::bind operator is a great thing. Being able to bind without making garbage... sweet.
Won't symbols solve this problem?
I can't see how
Something like this perhaps? It won't clash stores and can be "tree shaken" XD => jsbin.com/verimut/1/edit?js
Man, JavaScript is such as mess that developers need to think about things like this.
How bout we all just stop using classes in our code? Problem solved.
Great video, but all proposals have different flavors of beeing sh*tty :)
A Tree Shaker is (should be) able to remove unused methods. I dont see the problem.
For the reasons mentioned in the video: it isn't statically analysable.
okay. I looked the video again. They mentioned that each method is exported individually. So Rollup & co can use a simple approach to remove unused imports. But a Code analysor knows the callgraph of the whole code and can remove unused methods (and classes). More compilcated, because the code needs to be analyzed but not impossible.
What about the instance[str.split('').reverse().join('')] example in the video?
Yes, right - no chance for a code analysor. I silently assumed that this kind of "Reflection" stuff (counterpart in Java or C#) will not be used. (I come Java/C#/C++ and would accept that dead code remover would fail in this case)
Ah, I am not being fooled again. This is an April fools joke right. Nobody would ever believe this, not even funny.