Hi folks,
I got a question about a function builder diagnostic.
Our deep learning library defines a Sequential
type for sequentially composing neural network layers. It's powered by a function builder called LayerBuilder
.
Sequential
is used like this:
let model = Sequential {
Conv2D<Float>(...) // first layer, feeding output to:
AvgPool2D<Float>(...) // second layer, feeding output to...
Flatten<Float>()
Dense<Float>(...)
Dense<Float>(...)
}
And ideally, we'd like the function builder to infer layer generic parameters so that only one needs to be specified:
let model = Sequential {
Conv2D<Float>(...) // only one `<Float>` specified
AvgPool2D(...)
Flatten()
Dense(...)
Dense(...)
}
But it doesn't work. Here's the error:
seq.swift:43:27: error: static method 'buildBlock' requires the types 'Dense<Float>.Output' (aka 'Tensor<Float>') and 'Dense<_>.Input' (aka 'Tensor<_>') be equivalent
let modelBad = Sequential {
^
seq.swift:30:15: note: where 'L1.Output' = 'Dense<Float>.Output' (aka 'Tensor<Float>'), 'L2.Input' = 'Dense<_>.Input' (aka 'Tensor<_>')
static func buildBlock<L1: Layer, L2: Layer>(_ l1: L1, _ l2: L2) -> Sequential<L1, L2>
^
seq.swift:45:3: error: generic parameter 'Scalar' could not be inferred
Dense()
^
seq.swift:7:14: note: 'Scalar' declared as parameter to type 'Dense'
struct Dense<Scalar>: Layer {
^
seq.swift:45:3: note: explicitly specify the generic arguments to fix this issue
Dense()
^
<Any>
Is this expected behavior?
Direct calls to LayerBuilder.buildBlock
do type-check when some generic parameters are inferred, which made me wonder whether this is a type inference deficiency specific to function builders:
// A direct call to the function builder entry point does compile:
_ = LayerBuilder.buildBlock(
// generic parameter specified only once:
Dense<Float>(),
Dense())
Minimized reproducer
Uncomment FIXME
code for error.
protocol Layer {
associatedtype Input
associatedtype Output
}
struct Tensor<Scalar> {}
struct Dense<Scalar>: Layer {
typealias Input = Tensor<Scalar>
typealias Output = Tensor<Scalar>
}
struct Sequential<L1: Layer, L2: Layer>: Layer {
var layer1: L1, layer2: L2
typealias Input = L1.Input
typealias Output = L2.Output
init(_ layer1: L1, _ layer2: L2) {
self.layer1 = layer1
self.layer2 = layer2
}
init(@LayerBuilder layers: () -> Self) {
self = layers()
}
}
@_functionBuilder
struct LayerBuilder {
static func buildBlock<L1: Layer, L2: Layer>(_ l1: L1, _ l2: L2) -> Sequential<L1, L2>
where L1.Output == L2.Input {
Sequential(l1, l2)
}
}
// Example:
let model = Sequential {
Dense<Float>()
Dense<Float>()
}
// FIXME: this doesn't compile:
// let modelBad = Sequential {
// Dense<Float>()
// Dense()
// }
// Ideally, we'd like to infer generic parameters so that they only need to be specified once.
//
// Not ideal:
//
// let model = Sequential {
// Conv2D<Float>(...)
// AvgPool2D<Float>(...)
// Flatten<Float>()
// Dense<Float>(...)
// Dense<Float>(...)
// }
//
// Ideal:
//
// let model = Sequential {
// Conv2D<Float>(...) // generic parameter specified only once
// AvgPool2D(...)
// Flatten()
// Dense(...)
// Dense(...)
// }
// A direct call to the function builder entry point does compile:
_ = LayerBuilder.buildBlock(
// generic parameter specified only once:
Dense<Float>(),
Dense())