Обязательный и один из нескольких идиом

Поддержка Kotlin DSL великолепна, но я столкнулся с двумя сценариями, которые могу только добавить обходным путем. Оба обходных пути имеют свой главный недостаток, поскольку они применяют ограничения только во время выполнения.

Первое ограничение: обязательный параметр

Я хотел бы написать что-то вроде этого:

start {
    position {
        random {
            rect(49, 46, 49, 47)
            rect(50, 47, 51, 48)
            point(51, 49)
        }
    }
}

где position является обязательным параметром. Мой подход заключается в том, чтобы установить позицию в нулевое значение при запуске и проверить ее при создании начального объекта.

Второе ограничение: одно из многих

Я хотел бы разрешить ровно один из нескольких возможных подобъектов:

start {
    position {
        random {
            [parameters of random assign]
        }
    }
}

or

start {
    position {
        user {
            [parameters of user assign]
        }
    }
}

У меня такое ощущение, что я достиг предела возможностей инструментария Kotlin DSL, потому что эти требования также проверяются только во время компиляции в основном языке.

Есть идеи?


person Balage1551    schedule 06.01.2019    source источник
comment
Как насчет position<User> { } и position<Random> { } ?   -  person Jonas Wilms    schedule 06.01.2019
comment
Можете ли вы добавить больше контекста к вашему вопросу? Я предполагаю, что вы реализуете какой-то безопасный строитель? Может быть, показать определения функций.   -  person nPn    schedule 06.01.2019
comment
@JonasWilms Это вариант, но он публикует какой-то неприятный синтаксис (я планирую этот DSL для не-разработчиков). Кроме того, он не будет предлагать другой контекст построителя на основе общего параметра.   -  person Balage1551    schedule 06.01.2019
comment
@nPn Я дам немного (скоро), но мои нынешние сборщики не справляются с проблемами.   -  person Balage1551    schedule 06.01.2019
comment
@ Balage1551 не-разработчики также не смогут интерпретировать ошибки типа Kotlin, поэтому я бы использовал здесь ошибки времени выполнения и модульные тесты.   -  person Jonas Wilms    schedule 06.01.2019


Ответы (2)


Вы можете черпать вдохновение из собственного HTML DSL Kotlin. Для обязательных аргументов используйте простые функции с аргументами, а не литерал функции с получателем.

Ваш DSL будет выглядеть примерно так:

start(
    position {// This is mandatory
        random {// This is not

        }
    }
)

И ваш start строитель:

fun start(position: Position): Start {
    val start = Start(position)
    ...
    return start
}

Используйте тот же подход для position().

person Alexey Soshin    schedule 06.01.2019
comment
Спасибо за ответ. Я пришел к такому же выводу (см. Вариант 1 в моем ответе. У него есть некоторые недостатки ясности, когда требуемое значение сложное, поэтому я пробовал и другие способы. Каждый из них имеет свою стоимость, и каждый может лучше соответствовать различным сценариям. Я до сих пор не решил, что лучше всего подходит для моей цели. - person Balage1551; 07.01.2019

Поразмыслив над проблемой, я понял, что эти два требования не могут быть решены в самом Котлине, поэтому чисто синтаксическое решение в представленной выше текущей форме невозможно. Однако есть несколько вариантов, которые могут создавать достаточно близкий синтаксис и решать одну или обе проблемы одновременно.

Вариант 1: Параметры

Это решение довольно простое и уродливое, добавляя ужасную аномалию «где-закрывающая скобка». Он просто перемещает свойство position в конструктор:

start(random {
    rect(49, 46, 49, 47)
    rect(50, 47, 51, 48)
    point(51, 49)
}) {
    windDirection to NORTH
    boat turn (BEAM_REACH at STARBOARD)
} 

Это просто в коде:

    fun start(pos : StartPosition, op: StartConfigBuilder.() -> Unit) : StartConfigBuilder 
             = StartConfigBuilder(pos).apply(op)

и создает функции построения верхнего уровня для реализации позиции:

fun random( op : RandomStartPositionBuilder.() -> Unit) = RandomStartPositionBuilder().apply(op).build()

class RandomStartPositionBuilder {
    private val startZoneAreas = mutableListOf<Area>()

    fun rect(startRow: Int, startColumn: Int, endRow: Int = startRow, endColumn: Int) =
            startZoneAreas.add(Area(startRow, startColumn, endRow, endColumn))

    fun point(row: Int, column: Int) = startZoneAreas.add(Area(row, column))

    fun build() = RandomStartPosition(if (startZoneAreas.isEmpty()) null else Zone(startZoneAreas))
}

fun user( op : UserStartPositionBuilder.() -> Unit) = UserStartPositionBuilder().apply(op).build()

class UserStartPositionBuilder {

    fun build() = UserStartPosition()
}

Хотя это решает как обязательные, так и единственные проблемы времени редактирования, делает DSL намного труднее для чтения, и мы теряем элегантность инструментов DSL. Это станет еще более запутанным, если в конструктор нужно будет переместить более одного свойства или если внутренний объект (позиция) станет более сложным.

Вариант 2: Инфиксная функция

Это решение перемещает требуемое сложное поле за пределы блока (это «неприятная» часть) и использует его как инфиксную функцию:

start {
    windDirection to NORTH
    boat turn (BEAM_REACH at STARBOARD)
} position random {
    rect(49, 46, 49, 47)
    rect(50, 47, 51, 48)
    point(51, 49)
}

or 

start {
    windDirection to NORTH
    boat turn (BEAM_REACH at STARBOARD)
} position user {
}

Это решение решает «единственную» проблему, но не «точно одну».

Для этого я модифицировал конструкторы:

//Note, that the return value is the builder: at the end, we should call build() later progmatically
fun start(op: StartConfigBuilder.() -> Unit) : StartConfigBuilder = StartConfigBuilder().apply(op)


class StartConfigBuilder {
    private var position: StartPosition = DEFAULT_START_POSITION
    private var windDirectionVal: InitialWindDirection = RandomInitialWindDirection()

    val windDirection = InitialWindDirectionBuilder()
    val boat = InitialHeadingBuilder()

    infix fun position(pos : StartPosition) : StartConfigBuilder {
        position = pos
        return this
    }

    fun build() = StartConfig(position, windDirection.value, boat.get())
}

// I have to move the factory function top level
fun random( op : RandomStartPositionBuilder.() -> Unit) = RandomStartPositionBuilder().apply(op).build()

class RandomStartPositionBuilder {
    private val startZoneAreas = mutableListOf<Area>()

    fun rect(startRow: Int, startColumn: Int, endRow: Int = startRow, endColumn: Int) =
            startZoneAreas.add(Area(startRow, startColumn, endRow, endColumn))

    fun point(row: Int, column: Int) = startZoneAreas.add(Area(row, column))

    fun build() = RandomStartPosition(if (startZoneAreas.isEmpty()) null else Zone(startZoneAreas))
}

// Another implementation 
fun user( op : UserStartPositionBuilder.() -> Unit) = UserStartPositionBuilder().apply(op).build()

class UserStartPositionBuilder {

    fun build() = UserStartPosition()
}

Это решает проблему «только одной» реализации почти элегантным способом, но не дает ответа на параметр «требуемое свойство». Так что это хорошо, когда можно применить значение по умолчанию, но все же дает только исключение времени анализа, когда позиция отсутствует.

Вариант 3: Цепочка инфиксных функций

Это решение является вариантом предыдущего. Чтобы решить требуемую проблему предыдущего, мы используем переменную и промежуточный класс:

var start : StartWithPos? = null

class StartWithoutPos {
    val windDirection = InitialWindDirectionBuilder()
    val boat = InitialHeadingBuilder()
}

class StartWithPos(val startWithoutPos: StartWithoutPos, pos: StartPosition) {
}

fun start( op: StartWithoutPos.() -> Unit): StartWithoutPos {
    val res = StartWithoutPos().apply(op)
    return res
}

infix fun StartWithoutPos.position( pos: StartPosition): StartWithPos {
    return StartWithPos(this, pos)
}

Тогда мы могли бы написать следующий оператор на DSL:

start = start {
    windDirection to NORTH
    boat heading NORTH
} position random {
}

Это решило бы обе проблемы, но за счет дополнительного присвоения переменной.

Все три решения работают, добавляют немного грязи в DSL, но можно выбрать то, что подходит лучше.

person Balage1551    schedule 06.01.2019