Matrix Validation DSL
Koma provides definitions for a domain-specific language that you can use to validate matrices used as inputs to your functions in a consistent way. The rules throw detailed, human-readable exceptions using an appropriate Java exception class based on rules that look like this:
import koma.* import koma.extensions.* import koma.util.validation.* fun mFunction(foo: Matrix<Double>, bar: Matrix<Double>, baz: Matrix<Double>) { validate { foo("foo") { 1 x 'N'; transposable } bar("bar") { 'N' x 'N'; symmetric } baz("baz") { 'N' x 1 ; max = 5 } } /* Your code here */ }
Some of the exceptions the above code could generate include:
java.lang.IndexOutOfBoundsException: Invalid matrix dimensions. Matrix Required Actual ====== ======== ====== foo 1xN 1x2 bar NxN 2x2 baz Nx1 2x3 baz must have the same number of rows as foo has columns baz must have the same number of rows as bar has rows baz must have the same number of rows as bar has columns baz must have exactly 1 columns (has 3)
or
java.lang.IndexOutOfBoundsException: bar must be symmetric, but has dimensions 1x2
or
java.lang.IllegalArgumentException: baz[0, 0] > 5.0 (value was 15.0)
Annotated Syntax
fun myFunction(foo: Matrix<Double>, bar: Matrix<Double>, baz: Matrix<Double>) { validate { /* vvv ------------------------------------ Matrix to examine. vvvvv ------------------------------ Name to use in the exception. vvvvvvvvvvvvvvvvvvvvvvvv --- Rules to check the matrix */ foo("foo") { 1 x 'N'; transposable } } }
Rules
Rules are regular Kotlin statements, which can be separated either by semicolons or newlines. The rules block will be evaluated with an instance of ValidaitonContext
as its receiver.
Syntax | Description |
---|---|
1 x 2 or dim(1, 2) |
Verify the matrix has 1 row and 2 columns. Values can be any expression that evaluates to Int . The latter syntax is provided as an alternative if the order-of-operations for infix functions does something weird. |
1 x 'N' |
Verify the matrix has 1 row and any number of columns; compare the column count to other things that use the character 'N' |
transposable |
The given dimensions can be in either order. So, a 1 x 3; transposable matrix can have either 1 row and 3 columns or 3 rows and 1 column. |
symmetric |
Verify that the matrix is symmetric |
max = 4.0 |
Specify a maximum allowable value for individual coefficients in the matrix. Can be any expression that evaluates to Double |
min = 2.0 |
Specify a minimum allowable value for individual coefficients in the matrix. Can be any expression that evaluates to Double |
Shorthand for a single Matrix
If you've written a function that only has a single Matrix argument, you can use this shorthand syntax instead of a full validate block.
fun myFunction(foo: Matrix<Double>) { foo.validate("foo") { 1 x 3; transposable } /* Your code here */ }
A Cautionary Example
If you have more than one matrix, you should avoid the shorthand syntax because dimensions variables will not "stick" as you might expect. For example:
fun myFunction(foo: Matrix<Double>, bar: Matrix<Double>) { foo.validate("foo") { 1 x 'N'; transposable } bar.validate("bar") { 'N' x 'N' } // <-- WILL NOT WORK /* Your code here */ }
In that example, it will validate that bar
is square, but not that its dimensions correspond with the number of columns in foo
as you might expect. This is because each validate block allocates a separate ValidationContext
that performs some of its validations at the end of the block.
How it works
Behind the scenes, the validation code uses kotlin extension methods to enable a syntax inspired by Kotlin's "Type-safe builders" feature.
Each of the validation rules you define is calling an extension method on the ValidationContext
class.
If you'd like to extend the validation syntax yourself, you can do so by adding more extension methods to ValidationContext
. bounds.kt
probably offers the best example to work from.