Hacker News new | ask | show | jobs
by AriaMinaei 2862 days ago
Optic [0] caught my eye. They seem to have made possible a new kind of abstraction in code. Something that's different from both functions and macros. It's a kind of abstraction that can be arbitrarily customized at each call site, yet retain its identity across all of them.

Example:

  foo = do_a()
  bar = do_b(foo)
Example 2:

  foo = do_a()
  bar = do_z(foo)
  baz = do_b(bar)
Example 3:

  foo = do_a()
  bar = foo + 10
  baz = do_b(bar)
The three examples have strong similarity in their first and last statements. There is a pattern there, but that pattern cannot be abstracted into a single macro or a function. So this pattern does exist, and is recognizable to the human eye, but the language does not allow one to express it.

What Optic seems to do is to recognize the pattern, create a single model out of it, and allow you to re-use that pattern elsewhere, or even transform it into new ones and re-use those new patterns elsewhere.

Again, this would be a new kind of abstraction. One whose leaks are easier to fix. You can have your cake and eat it too!

[0] https://useoptic.com

4 comments

Hey -- thanks for checking us out!

You're definitely understanding the premise of our parser. It evaluates a regex-like set of rules on an AST tree to match different forms of code. These rules can be recursive as you can see in the first example of our home page [0]. In that example Optic matches an express js route and the headers, parameters and responses inside it. The result is a nice json object with shape {method, url, parameters: [], headers: [], responses" []}. OOTB the JS interpreter couldn't do this because it doesn't encounter the calls to req.query.param_name until that code runs.

PG wrote about building the language/abstraction to fit your problem [1]. In an ideal world we all would do this, but in practice there's always a gap between our program and the abstraction we describe it in. Today that gap is only bridged by a human understanding the code. Until now...Optic is allowing us to programmatically deal with these implicit abstractions.

We believe most of the dev tools created over the next decade will be built on top of your code in a way that allows them to collaborate with real developers. To realize this world we need a programatic interface to read, generate and mutate code so we created Optic.

[0] https://useoptic.com [1] http://www.paulgraham.com/progbot.html

I was really fascinated by this as well. I think it's a great idea. Where does the 15 hours/per developer/per week come from. Couldn't find more information about that anywhere on your website.
Before I started Optic I watched 20 developers code for 1 day each. I kept logs of what they were working on and asked people to rate how they felt throughout the process. The 15/hrs/wk is a rough estimate of what we could automate. Our most avid users over the last month self report a little less but I think once we add better support for a few more things we’ll catch up.

I never thought to publish those results. Sounds like something people would be interested in?

I'm sure neither the technical-minded audience here wouldn't nor the prospective clients wouldn't mind having a look at them at all.
Would love to see your findings!
can someone ELI5 this? I am a former SW engineer, but havent read through code in a couple of years, so it can be a technical explanation. I just don't understand what is different about this than regular functions as first class citizens? I tried to get it but maybe there is an easier explanation for laymen-ish people.
You're right. My examples should've been more complicated for regular functions not to be a suitable abstraction. Now, assume that the examples are complicated. In that case:

One method to define that function would be to do it the way explained in this [0] comment. That is, express the solution with small, composable functions. This would work as long as you can express the program in a composable way. This is what I usually try to do first, unless I find expressing my solution with composable pieces is not worth the effort.

For example, an async solution to a problem would be very difficult to express composably without well thought-out building blocks. One such collection of building block is Reactive Extensions [1], which is the result of years of research and development. So, before RX and similar libraries existed, it was costly to express async programs composably. That cost may or may not have been worth it.

Another method to define that function, if we're not going for "composable," would be to write it with an `options` argument, which would look something like:

  type Options = {use_cache: boolean, skip_one_cycle: boolean, number_of_retries: number, ...}
The more options one function has, the lengthier its definition would get, which means its core logic could be obfuscated. So that'd be the tradeoff with this method.

Third method would be to define multiple variants of that function, like `do_things()` and `do_things_with_cache()` and `do_things_and_retry()`, etc. Here, each function is simpler than a single function taking an `options` arg, but then core logic would be repeated in all of them.

And since these three methods are not binary options, one may even use all three of them to some extent.

What Optic seems to provide is a fourth option which allows you to forgo defining that function (as long as that makes sense) and repeat your core logic in many different parts of your code. The devtool would then recognize the core logic in all those different repetitions and assign it a single identity, which would allow you to manage that core logic from a single place. This would be especially useful in a language that does not support macros.

(Btw, macros would be your fourth option if you're coding in lisp/rust/etc. The one tradeoff with macros is that they are yet another layer of abstraction that the author and the reader of the code should keep in their head.)

[0] https://news.ycombinator.com/item?id=17792770

[1] http://reactivex.io/

why not just

    function baz(modifier) {
       let foo = do_a();
       let bar = modifier(foo);
       return do_b(bar);
    }

    Example 1: baz((foo) => foo);
    Example 2: baz((foo) => do_z(foo));
    Example 3: baz((foo) => foo + 10);
which lets you be more concise with

    baz = (modifier) => do_b(modifier(do_a()))
Reminds me of profunctor lenses.