question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

CAUTION: There are still some caveats in the types, I will fix them but already wanted to showcase you the general idea of the API here.

I still feel the urge of rethinking our DI spike and the use of classes. Here is my suggestion on how to wire the Langium parts while maintaining the following features:

  • dependency injection (achieved by binding a curried function to an instance of Langium)
  • cyclic references are supported (e.g. linking, scoping and typesystem reference each other)
  • 100% transparency/documentation of the langium API in one place
  • allowing the user to extend the API/adding new objects that will be injected and can be used everywhere

Top level framework API

Here we have an overview what is Langium capable of. Provided the types are well documented in this central location, the user will get a good overview.

export type Langium = {
    linkingProvider: LinkingProvider,
    scopingProvider: ScopingProvider,
    validationProvider: ValidationProvider
}

// just examples
export type LinkingProvider = () => void
export type ScopingProvider = () => void
export type ValidationProvider = () => void

Functional dependency injection

These generic types and one generic function for injection is basically all we need.

// I think this self recursive type still needs to be tweaked a bit.
// We need to ensure that we only have types `Inject<..., ...>`,
// so `K in keyof Langium` might not be sufficient.
export type LangiumAPI<L extends LangiumAPI<L>> = {
    [K in keyof Langium]: Inject<L, Langium[K]>
}

export type Inject<L extends LangiumAPI<L>, T> = (api: L) => T

export function createLangium<L extends LangiumAPI<L>> (api: L): Langium {
    return Object.fromEntries(
        Object.entries(api).map(([key, value]) => [key, value(api)])
    ) as Langium;
}

Great defaults which can be reused

By exactly knowing our domain, we will provide one or more defaults for our users. This could be even an optional lib to keep the Langium core small.

export function defaultLangiumAPI<L extends LangiumAPI<L>>() { // TODO: declare self-recursive type
    return {
        linkingProvider: (api: L) => () => {},
        scopingProvider: (api: L) => () => {},
        validationProvider: (api: L) => () => {}
    };
}

Typical use-site

The user typically needs to customize the defaults when developing non-trivial languages.

interface MyLangiumAPI extends LangiumAPI<MyLangiumAPI> {
    myCustomProvider: MyCustomProvider
}

type MyCustomProvider = (api: MyLangiumAPI) => unknown

 // look ma, custom api is injected into the default ScopingProvider API
const scopingProvider = (api: MyLangiumAPI) => () => {
    const { myCustomProvider } = api;
    // ...
}

// a user defined feature
const myCustomProvider = (api: MyLangiumAPI) => () => {
    // ...
}

const langium = createLangium<MyLangiumAPI>({
    ...defaultLangiumAPI(), // use the defaults
    scopingProvider, // re-define the default scoping provider
    myCustomProvider // add a custom provider
});

It would be great if we could consider such an JS-like approach instead of just transferring to Langium what we did in Xtext.


☝️ please take a look @msujew @spoenemann

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:22 (17 by maintainers)

github_iconTop GitHub Comments

1reaction
msujewcommented, May 18, 2021

Great work, that seems to work really well, although it looks like some black magic.

I tried it with my cyclic dependency example and this approach even works with constructor injection.

export interface MyLangium extends Langium {
    // defining cyclic dependencies
    a: A,
    b: B
}

class A {
    value: string;
    constructor(public b: B) { }
}

class B {
    value: number;
    constructor(public a: A) { }
}

const myLangium = inject<MyLangium>({
    ...defaultLangiumModule(),
    a: (api: MyLangium) => {
        const a = new A(api.b);
        a.value = 'text';
        return a;
    },
    b: (api: MyLangium) => {
        const b = new B(api.a);
        b.value = 24;
        return b;
    }
});

const a = myLangium.a;
console.log(a.value); // prints 'text'
console.log(a.b.value); // prints '24'
1reaction
danieldietrichcommented, May 17, 2021

The key for DI with Proxies (that allow cycles) like outlined above is using new Proxy(class {}, handler):

type Module<L extends object> = {
    [K in keyof L]: Inject<L, L[K]>
}

type Inject<L, T> = (api: L) => T

function inject<L extends object>(module: Module<L>): L {
    const api: L = new Proxy({} as any, {
        get: (_, prop) => _proxy(api, prop, module)
    })
    return api
}

function _proxy(api: any, prop: any, module: any) {
    return new Proxy(class {}, {
        apply: (target, thisArg, argList) => module[prop](api)(argList),
        construct: (target, argList) => new module[prop](api),
        get: (target, p) => module[prop](api)[p]
    })
}

The class {} target allows to be called as

  • function
  • constructor
  • object property (via get)

It is a trick but it works. I will update the example above.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Rethink Lab: ReTHINK Your Approach to DEI for Impact
Understand why DEI solutions must change to have impact. Learn about ReTHINK's innovative and research-driven approach to creating change in the workplace.
Read more >
Rethink App - Apps on Google Play
Welcome to the official hybrid platform for Rethink Events. We know that it's the personal connections you make at a summit which deliver ......
Read more >
Blink Consulting: Critically Rethinking Diversity
Blink Strategic Consulting collaborates with schools and other organizations to critically, compassionately and strategically rethink diversity, equity and ...
Read more >
Rethink App on the App Store
Welcome to the official hybrid platform for Rethink Events. We know that it's the personal connections you make at a summit which deliver ......
Read more >
It's time to rethink DEI - Institute for Inclusive Leadership
In his must-read book, Amri offers a guide to better understanding the historical context of inclusion, a rethinking of the efforts organizations are ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found