DEV Community

Tea Reggi
Tea Reggi

Posted on

I'm looking for a new coding paradigm.

A couple months ago I stumbled down a rabbit hole. I had a strong desire to create a new sort of JavaScript / TypeScript coding paradigm. I still to this day have a hard time trying to articulate what I want or why I want it. I feel I have a hard time maintaining and writing code, because I can't express myself fully.

If there's anything out there similar to this idea, please let me know. I'd love to learn what's out there.

Here are some of ways of trying to explain it:

  • I want to create a library functions that provide functionality like building blocks
  • I want to be able to "tap" into different values.
  • I'm looking for something that is functional, or event based
  • I need to support async and sync operations

A couple of months ago I created a module profound, which is currently broken, but you can play around with the demo code on repl.it.

import { placeholder, profound } from '@reggi/profound'

const alpha = profound({}, ({ }) => 'hello world I\'m a profound')
// console.log(alpha()) // hello world I\'m a profound

const beta = profound({ alpha }, ({ alpha }) => `I am using alpha as a dependency (${alpha})`)
// console.log(beta()) // I am using alpha as a dependency (hello world I'm a profound)

const gamma = profound({ alpha, beta }, ({ alpha, beta }) => `
    Gamma needs alpha and beta to run.
    Rather than being very redundant and running both in here. They are fetched for use. 
    If you pass alpha or beta into gamma, they are not run. 
    This function also takes any arguments that alpha, or beta need.
    Profounds become async: 
        1. If their dependencies are async and are being run (input dependent)
        2. If this callback is async
    (${alpha} ${beta})
`)

const age = placeholder<number>()('age')

const delta = profound({ age, gamma }, ({ age, gamma }) => gamma.length + age)

console.log(delta({ age: 30 })) // 514
Enter fullscreen mode Exit fullscreen mode

It may not be obvious at first what's happening here, so let me walk you through.

  1. The result from a profound is a function.
  2. profound has two arguments, one is a list of dependent functions, the other is a callback. The callback signature is (args) => result, where args is an object mapping the results from the dependencies.
  3. A placeholder is a stub for something that can't be derived from a function, has to be entered by a user.

In a nutshell, profound is a resolver that uses keyed objects to store value and pass results along. You "tap" into dependencies, rather than running them explicitly.

This allows you to "compose" functions.

Somehow I magically got this approach to work with TypeScript, so that the arguments for the outputted functions "compile" and include the tree of "dependent" functions.

I tried to push this to it's limit by creating a really long chain and seeing what would happen, and it broke. Leading to an TypeScript Error:

Type instantiation is excessively deep and possibly infinite.

I've written up a StackOverflow issue to try and figure out a way of preventing this error from happening.

import {profound} from './profound';

export const example0 = profound({}, () => 'anything');
export const example1 = profound({example0}, () => 'anything');
export const example2 = profound({example1}, () => 'anything');
export const example3 = profound({example2}, () => 'anything');
export const example4 = profound({example3}, () => 'anything');
export const example5 = profound({example4}, () => 'anything');
export const example6 = profound({example5}, () => 'anything');
export const example7 = profound({example6}, () => 'anything');
export const example8 = profound({example7}, () => 'anything');
export const example9 = profound({example8}, () => 'anything');
export const example10 = profound({example9}, () => 'anything');
export const example11 = profound({example10}, () => 'anything');
export const example12 = profound({example11}, () => 'anything');
export const example13 = profound({example12}, () => 'anything');
export const example14 = profound({example13}, () => 'anything');
export const example15 = profound({example14}, () => 'anything');
export const example16 = profound({example12, example13, example14, example15}, () => 'anything');
export const example17 = profound({example16, example13, example14, example15}, () => 'anything');
Enter fullscreen mode Exit fullscreen mode

I'd love to feel like I can compose code in some sort of way that makes it feel more like building blocks rather then tons of repetitive code.

What I'm trying to prevent is code like this:

function alpha (a) {
  return a
}

function beta (a, b) {
  return alpha(a) + b
}

function gamma (a, b, c) {
  return alpha(a) + beta(b) + c
}
Enter fullscreen mode Exit fullscreen mode

Which may seem silly, but seems to happen a lot:

Here's a more real world example:


function validateEmail (email) {
  return email
}

async getDomain (email) {
  // ignore details
  return 'gmail.com'
}

// what should the arguments to this be?
async sendMessageFromEmail (email) {
  const domain = getDomain(email)
  if (domain == 'gmail.com') return 'sentEmail'
  return null
}

//or 

async sendMessageFromEmailWithDomain (email, domain) {
 if (domain == 'gmail.com') return 'sentEmail'
 return null
}

// or a hybrid

async sendMessage (email, domain?) {
 domain = domain ?? getDomain(email)
 if (domain == 'gmail.com') return 'sentEmail'
 return null
}
Enter fullscreen mode Exit fullscreen mode

You can see here sendMessage has a couple of permutations, this is a dumb example, but does raise something I see in a lot of code.

In Conclusion

I'd love to hear thoughts about this sort of pattern, and other ways to handle the case above. Please leave a comment below, or reach out on twitter @thomasreggi. Thanks for reading!

Top comments (7)

Collapse
 
reggi profile image
Tea Reggi • Edited

Was just tinkering around with this idea again and still has the same type-error issue:

function profound<
  T extends Record<string, (...args: any[]) => any>,
  TE extends IsEmptyObject<T>,
  D extends { [K in keyof T]: ReturnType<T[K]> },
  DE extends IsEmptyObject<D>,
  P extends { [K in keyof T]: Parameters<T[K]>[0] },
  PE extends IsEmptyObject<P>,
  R extends P[keyof T],
  CB extends (dependencies: Required<D>) => any,
  AG extends PE extends true ? undefined : undefined extends R ? D : D & R
>(dependencies: T, callback: CB) {
  return (args: Partial<AG>): ReturnType<CB> => {
    const resolvedDeps: any = {}
    for (const [key, dependency] of Object.entries(dependencies)) {
      if (args && args[key] !== undefined) {
        resolvedDeps[key] = args[key]
      } else {
        resolvedDeps[key] = dependency(resolvedDeps)
      }
    }
    return callback(resolvedDeps)
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vonheikemen profile image
Heiker

This sounds a lot like the reader monad. Take a look at this implementation. Maybe that's what you're looking for, or maybe you can use it as a base for what you want to do.

Collapse
 
reggi profile image
Tea Reggi • Edited

Wow per your recommendation, I've been playing around with crocs it seems like a really useful library and there is much to learn from it, I think there's a lot here to work with and possibly build on. It does lack TypeScript support though 😭. Thank you so much for the recomendation!

Collapse
 
vonheikemen profile image
Heiker

Right... typescript. I don't know much about it but I understand that these FP patterns are not easy to do with typescript. But anyway, might want to take a look at fp-ts.

Collapse
 
coryetzkorn profile image
Cory Etzkorn
Type instantiation is excessively deep and possibly infinite.

... is truly a great error message.

Collapse
 
reggi profile image
Tea Reggi

Get your mind out of the gutter.

Collapse