Azure / Azure/bicep

Lambda as a top-level data type

Open
#15,387 8 comments 0 reactions 1 assignee Claimed by @jeskew View on GitHub
enhancement
Dominant language
Bicep
Stars
3.6k
Forks
830
Avg merge
1d 2h
Merged PRs (30d)
79

Description

# Is your feature request related to a problem? Please describe.
I maintain a set of modules that export a set of types that overlap in their interfaces, both in terms of data structure and functionality.
While I can declare the type data structures in a way that lets me use "duck typing" to unify them:
```bicep
// interface.bicep
type MyInterface = {
sharedField: string
}

// a.bicep
type A = {
sharedField: string
aField: bool
}

// b.bicep
type B = {
sharedField: string
bField: object
}
```
Which allows me to pass either an `A` or a `B` into a method that expects an `Interface`. And if i'm missing a field it expects, i will get errors letting me know that i'm passing in an incompatible object.
```bicep
func interfaceCall(interface MyInterface) any => doStuff(interface.sharedField)

var a = {
sharedField: 'a'
otherField: false
}

// success
interfaceCall(a)

// build errors
interfaceCall({})
```
However, if I want `Interface` to define not just the properties, but also member methods, there's really not a way to do this today. There's no way to express that the interface expects a method to be present. Best I can do is to export the same function name in all my interface-bound modules.

```bicep
// a.bicep
type A = { /* same as above*/ }

@export()
func interfaceCall(interface MyInterface) any => // A-specific operation

// b.bicep
@export()
type B = { /* same as above*/ }

@export()
func interfaceCall(b B) any => // B-specific operation
```
But now, I have to know what import brought in what functions in order to call them

```bicep
// main.bicep
import * as A from './a.bicep'
import * as B from './b.bicep'

param MyInterface x
param B b

x.interfaceCall(x) // syntax error

A.interfaceCall(x) // maybe correct? I don't actually know what x is
B.interfaceCall(b) // correct
```

# Describe the solution you'd like

I would love to hear what the thoughts are on adding `lambdas` to the top-level data type set, since they are already present in the built-in functions like `map`.
* signature could be `lambda(paramType*) outputType`
* same limitations as `func` would be reasonable:
* no `var` references
* no resource references

Using this proposal I can express the examples above as
```bicep
// interface.bicep
@export()
type MethodType = lambda(MyInterface) any

@export()
type Type = {
sharedField: sharedFieldType
method: MethodType
}

// a.bicep
import * as Interface from './interface.bicep'

type A = {
sharedField: string
method: Interface.MethodType
aField: bool
}

func interfaceCall(a A) any => // A-specific operation

@export()
func new(o object) A => union({method: interfaceCall}, o)

// b.bicep
import * as Interface from './interface.bicep'

type B = {
sharedField: string
method: Interface.MethodType
bField: object
}

func interfaceCall(b B) any => // B-specific operation

@export()
func new(o object) B => union({method: interfaceCall}, o)

// main.bicep
import * as A from './a.bicep'
import * as B from './b.bicep'

module _ './my-module.bicep' = {
name: '_'
params:{
interface: A.new( {
sharedField: 'a'
aField: true
})
}
}

// my-module.bicep
import * as Interface from './interface.bicep'

param interface Interface

// doesn't have to know what concrete type the object is
var result = interface.method(interface)
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.