kotlin-style-guide icon indicating copy to clipboard operation
kotlin-style-guide copied to clipboard

Убрать запрет на expression body в общем случае

Open PilotOfSparrow opened this issue 2 years ago • 33 comments

Чтобы можно было так:

    private fun moo(mo: String) = when (mo) {
        "you",
        "and",
        "me" -> true
        else -> false
    }

    private fun foo() =
        try {
            if ("ohhh" == "you touch my talalam") {
                throw IllegalStateException("It's din din dong")
            }
            "magic"
        } catch (e: Exception) {
            println("ta talala talalalala")

            "the gathering"
        }

Сейчас можно только так:

    private fun moo(mo: String): Any {
        return when (mo) {
            "you",
            "and",
            "me" -> true
            else -> false
        }
    }

    private fun foo(): String {
        return try {
            if ("ohhh" == "you touch my talalam") {
                throw IllegalStateException("It's din din dong")
            }
            "magic"
        } catch (e: Exception) {
            println("ta talala talalalala")

            "the gathering"
        }
    }

PilotOfSparrow avatar Oct 11 '21 13:10 PilotOfSparrow

Возможно, вкусовщина, но я бы использовал expression body только в случаях когда у функции "одно тело", то есть when, with, repeat и т.д. а для if ... else, try ... catch использовал бы block body.

Первый вариант для меня выглядит более читаемо и по краткости не уступает:

fun blockFoo() {
    if (...) {
        // ...
    } else {
        // ...
    }
}


fun expressionFoo() =
    if (...) {
        // ...
    } else {
        // ...
    }

osipxd avatar Oct 11 '21 13:10 osipxd

Я бы добавил ещё чтобы можно было реактивные цепочки писать через expression body - вполне читаемо и уже привычно для многих

Ну и раз when через expression body, то и if-else тоже через него - в обоих случаях ветвление и возврат результата, к тому же expression body позволит писать однострочные if-else для коротких, но часто используемых функций

xd720p avatar Oct 29 '21 05:10 xd720p

Я за разрешение expression body в полной мере.

Куда важнее на мой взгляд, чтобы группа методов в одном файле была оформлена в едином стиле для простоты восприятия, а не expression body vs. block body.

mrblrrd avatar Oct 29 '21 07:10 mrblrrd

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

Без жёсткого правила, всегда будут появляться такие варианты:

    private fun moo(mo: String) = 
        when (mo) {
            "you",
            "and",
            "me" -> true
            else -> false
        }
    // Нет закрывающей скобки уровня функции.

    fun chain() = some
        .call()
        .another()
        .andOneMore()

    // Тоже не видно где закончится функция, ТК нет скобки на уровне. А цепочка может быть очень сложной.

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

Для меня, использовать почти всегда блок бади - это однотипность.

А экспрешн стоит только для очень редких случаев типа ссылки на другую функцию или явного возврата значения, когда в одну строку. То есть когда блок явно сильно увеличил бы количество строк.

Jeevuz avatar Nov 05 '21 23:11 Jeevuz

@Jeevuz, а как насчёт варианта когда expression не переносится? Например when или with:

// Когда функция содержит только when
fun handleEvent(event: Event) = when(event) {
    is ShowMessage -> showMessage(event.message)
    is ShowError -> showError(event.error)
}

// Или если она конфигурирует что-то
fun initUi() = with(binding) {
    saveButton.setOnClickListener { viewModel.onSaveClicked() }
    // ...
}

В этом случае:

  1. Всё еще явно видно где заканчивается функция
  2. На один уровень вложенности меньше

osipxd avatar Nov 09 '21 08:11 osipxd

@osipxd Да, этот вариант хороший, мне он нравится. Все равно видно конец тела функции. Главное, чтобы это четко соблюдалось.

И плюс возникает вопрос тогда что с этим делать, разрешать или нет:

fun initText() = binding.content.textView.apply {
    saveButton.setOnClickListener { viewModel.onSaveClicked() }
    // ...
}

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

fun initText() = binding
    .content
    .textView
    .apply {
        saveButton.setOnClickListener { viewModel.onSaveClicked() }
        // ...
    }

И придется переделывать в блок. А лишние телодвижения из-за стайлгайда - ну такое..

Jeevuz avatar Nov 16 '21 09:11 Jeevuz

Есть предложение сойтись на таком:

fun singleLine() = just.single().line().ofCode().isPerfectlyOk 

fun singleHigherOrderFunctionWithLambda() = higherOrderFunctionWithLambda(params) { 
  ...
} 

fun singleExpressionWithCurlyBrackets() = when(something) { 
  ...
} 

То есть:

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

Jeevuz avatar Jan 10 '22 16:01 Jeevuz

Можно ещё добавить в качестве предостережения, что если выражение используемое с функцией со скобками сложное, стоит использовать функцию с блоком (Как-то коряво сформулировал) Хочется избежать подобного:

val someFun() = when(getNumberForType(arg.mapToUi().type)) {
    0 -> ...
    1 -> ...
    else -> ...
}

osipxd avatar Jan 12 '22 07:01 osipxd

Ну кстати это не кажется сложночитаемым. Скорее просто не красиво :) Тут бы это вынести в параметр и сетить снаружи. Имхо, можно это не ограничивать. Но добавить, что открывающая скобка должна оставаться на одной строке с fun, тогда длина строки ограничит такие штуки.

Jeevuz avatar Jan 12 '22 07:01 Jeevuz

Да, норм

osipxd avatar Jan 12 '22 08:01 osipxd

Предлагаю не маскирвоать expression body под block body и преимущественно всегда делать перенос после равно =. Кроме случаев, когда помещается в одну строку. Расскажите, почему вы хотите, чтобы fun был на одном уровне с }, чтобы выглядело как будто это block body?

mrblrrd avatar Jan 12 '22 10:01 mrblrrd

Да, чтобы мимикрировать. Основной поинт - наличие закрывающей скобки на уровне с fun. Если перенести после =, то закрывающая будет сдвинута и тяжелее читать. Тогда уж лучше блок бади делать всегда.

Просто почему неудобно выше писал, вот https://github.com/RedMadRobot/kotlin-style-guide/issues/7#issuecomment-962281342

Jeevuz avatar Jan 12 '22 12:01 Jeevuz

Я неоднозначено вопрос сформулировал. Мне интересно, почему мимикрировать – это хорошо? Только потому что «читать легче», или есть какие-то еще причины?

На мой взгляд плохо, когда две разные по смыслу конструкции пытаются выглядеть одинаково. Цель использования expression body – подчеркнуть, что функция возвращает значение определенного типа. Block body – шире, функция может не возвращать значение, а делать какой-то сайд эффект. Это смысловое разделение дает читающему больше информации: что от функции ожидать и как ее анализировать. Если их форматировать по-разному, то легче будет понять какого вида функция и, соответственно, намерение разработчика. Если мы будем пытаться форматировать их одинаково, то мы затрудним отделение одного от другого, надо будет вчитываться. С этой точки зрения читать наоборот будет труднее.

В общем, предлагаю сначала определить принцип формирования код стайла. Он нам в первую очередь для того, чтобы при чтении кода быстрее усваивать намерение и смысл, и только во вторую очередь эстетика? Или наоборот? Если мы с этим принципом не определимся, то мы будем часто скатываться в обсуждение эстетических предпочтений, которые у каждого свои.

mrblrrd avatar Jan 12 '22 12:01 mrblrrd

Для меня причина мимикрии как раз смысловая. Закрывающая скобка на уровне fun подчёркивает, что это функция, а не свойство. Например такие конструкции выглядят почти одинаково:

val lastState: State
    get() = {
        // ...
    }

fun renderState(state: State)
    = with(binding) {
        // ...
    }

На мой взгляд такая запись функции больше подвержена ошибкам из-за того что возвращаемый тип можно не указать и тогда он выводится из возвращаемого значения поэтому и при этом не имеет плюсов. Из-за этого же опасно использовать такой тип записи для разделения функций на те который возвращают значения и те которые не возвращают.

С точки зрения смысла лучше всегда использовать block-body, а использование expression-body это отхождение от смысла ради красоты и уменьшения визуальной вложенности. На мой взгляд приемлемо только в случае когда выражение, которое оказывается на одной строчке с объявлением функции, не содержит логики и его можно "вынести за скобки" и не обращать на него внимание при чтении тела функции. Отличный пример, как раз использование with(binding):

fun render(state: State) {
    return with(binfing) {
        // ...
    }
}

fun render(state: State) = with(binding) {
    // ...
}

osipxd avatar Jan 12 '22 14:01 osipxd

@mrblrrd а откуда у тебя такое понимание?

Цель использования expression body – подчеркнуть, что функция возвращает значение определенного типа.

Просто впервые такое вижу. Ведь можно написать

fun doSome() = if(true) { doSideEffect }

Я согласен с Осипом. Для меня тоже экспрешн бади это просто сахар. В основном для укорачивания мелких функций. Смыслового отличия от блок бади не вижу, вот честно.

А для читабельности хочется отличать функцию, видеть ее начало и конец, если она многострочная.

Jeevuz avatar Jan 12 '22 18:01 Jeevuz

Давайте сравним предлагаемое форматирование, когда перенос после =, а не до. То есть вместо

val lastState: State
    get() = {
        // ...
    }

fun renderState(state: State)
    = with(binding) {
        // ...
    }

Рассмотрим именно такую запись

val lastState: State
    get() = {
        // ...
    }

fun renderState(state: State) = 
    with(binding) {
        // ...
    }

Закрывающая скобка на уровне fun подчёркивает, что это функция

То что функция это функция подчеркивает ключевое слово fun 🤔 Мне трудно понять, зачем нужна дополнительная закрывающая скобка для усиления этого ключевого слова. Опущу лишнее для сравнения:

val lastState: State
    // ...

fun renderState(state: State) = 
    // ...

К тому же, закрывающая скобка на уровне fun создает впечатление, что это block body, сбивая с толку. Но об этом далее.

На мой взгляд такая запись функции больше подвержена ошибкам из-за того что возвращаемый тип можно не указать и тогда он выводится из возвращаемого значения поэтому и при этом не имеет плюсов.

Правильно понимаю, что это аргумент в пользу не использовать expression body в принципе, а не к вопросу делать перенос после = или писать в одну строку? Если про использование expression body в принципе, то согласившись, что опасность в такой записи есть, надо отказываться от нее совсем и убрать исключения из правил. Потому что возможность ошибки есть в любом случае, даже для исключений. Поэтому для меня странно, кстати, текущие использование expression body в исключении с View Binding:

abstract class BaseFragment : Fragment() {

    open fun initViews() = Unit

    // ...
}

И далее

class MyFragment : BaseFragment() {

    override fun initViews() = with(binding) {
        // ...
    }

    // ...
}

Странно это, потому что по дизайну возвращаемый тип метода initViews()Unit. И корректность переопределения сильно зависит от типа последнего выражения в нем. Ребята рассказывали, что иногда из-за этого приходится менять выражения местами, чтобы все сошлось. Проблему здесь я вижу не в самом expression body, а в том, что он не должен здесь использоваться в принципе. Когда возвращаемый тип – Unit, то все, что делает метод – это сайд эффект. Логика инициализации вью – это один большой сайд эффект на их состояние. Никакое значение эта логика вычислять и возвращать не должна. Соответственно, и переопределять метод надо только так:

class MyFragment : BaseFragment() {

    override fun initViews() {
        with(binding) {
            // ...
        }
    }

    // ...
}

И в родительском классе метод должен быть объявлен без использования выражения, которое ничего не возвращает:

abstract class BaseFragment {

    open fun initViews() {
        // Intentional empty.
    }

    // ...
}

Скорее всего сделано через = Unit из-за правила линта, которое запрещает пустые методы. Но в этом случае его допустимо подавить. Иначе Lint диктует некорректную по сути реализацию задуманного дизайна.

Откуда возникла опасность в данном случае? Из-за самого expression body? Нет, она возникла из-за того, что он используется не по назначению – для реализации метода, который по замыслу никакого значения не вычисляет. Я предлагаю разрешить expression body и использовать его по месту, т.е. для выражений. Это средство выразительности языка позволяет разработчику объявить в коде: «Хэй! Я тут хочу вычислять какое-то значение и возвращаю его, я спроектировал так, что здесь должно быть выражение».

Из-за этого же опасно использовать такой тип записи для разделения функций на те который возвращают значения и те которые не возвращают.

Выше я постарался вывести, откуда появляется «опасность». Здесь рассмотрим есть ли опасность в том, что тип выражения может поменяться. Когда expression body используется по назначению – объявить выражение, возвращающее значение – то у него всегда есть сторона получатель, которая ожидает определенный тип. Возвращая другой тип, мы получим ошибку компиляции. Это работает также как и для всех других выражений, функции-выражения в этом смысле ничем не отличаются. Я считаю, что никакой опасности нет. К тому же, при сложном expression body лучше всегда в явном виде прописывать return type, также как и в публичных интерфейсах.

С точки зрения смысла лучше всегда использовать block-body, а использование expression-body это отхождение от смысла ради красоты и уменьшения визуальной вложенности.

Множество случаев применения block body – надмножество случаев применения expression body. Expression body предназначен для частных случаев, когда требуется подчеркнуть, что по дизайну в коде должно быть именно выражение. Я предлагаю закрепить такой смысл за их применением. Приведенный фрагмент на мой вгляд изначально некорректный:

fun render(state: State) {
    return with(binfing) {
        // ...
    }
}

fun render(state: State) = with(binding) {
    // ...
}

Никакого return перед with(binding) быть не должно. И использовать здесь expression body не стоит.

Вернемся к тому, как форматировать

Если мы согласимся разделять expression body и block body по смыслу применения, то форматировать лучше так, чтобы их было легче отличать друг от друга. Приведу хорошие на мой взгляд примеры:

fun singleLine() = just.single().line().ofCode().isPerfectlyOk 

fun singleLineWithBreak() = 
    just.single().line().ofCode().isPerfectlyOk 


fun chain() = 
    just
        .single()
        .line()
        .ofCode()
        .isPerfectlyOk 

fun higherOrderFunctionWithLambda() = 
    higherOrderFunctionWithLambda(params) { 
        // ...
    } 

fun expressionWithCurlyBrackets() = 
    when(something) { 
        // ...
    } 

Как понять, где конец функции? Конец функции с expression body – это конец выражения. Как мы понимаем, где конец выражения? Также, как мы это делаем во всех других контекстах использования выражений. А эта пара fun и =:

fun /*higherOrderFunctionWithLambda()*/ = 

сразу бросается в глаза, и говорит о том, что далее идет выражение.

mrblrrd avatar Jan 12 '22 18:01 mrblrrd

@Jeevuz

а откуда у тебя такое понимание?

Оттуда, что использование выражения для сайд эффекта – это бессмыслица, разве нет? Я понимаю, что так можно записать, и Kotlin это позволяет. Но я предлагаю никогда так не делать:

fun doSome() = if(true) { doSideEffect }

А использовать выражение, там где мы хотим, чтобы было возвращаемое значение. Если подчинить эту конструкцию такой семантике, то мы увидим смысл в применеии expression body и не будем воспринимать это как сахар. При чтении кода будет легче понимать, что человек вкладывал в дизайн.

mrblrrd avatar Jan 12 '22 18:01 mrblrrd

Почему бессмыслица? Чем это отличается от

fun doSome() {
  if (true) {
    doSideEffect
  }
}

И там и там выражение для сайд эффекта. И там и там Unit.

Jeevuz avatar Jan 12 '22 18:01 Jeevuz

Если вкладывать смысл разделения expression и side-effect/statement (который я предлагаю вкладывать), то разница появится. Когда я вижу:

fun doSome() {
  if (true) {
    doSideEffect
  }
}

Мне и в голову не придет присваивать это к какой-нибудь переменной, или использовать в составе другого выражения. Более того, это мне разу сообщает, что внутри – сайд эффект. Поэтому стайл гайд сильнее, когда в нем семантика, облегающая понимание, а не только эстетика.

val abcd = doSome()

val abcd = 2 * doSome() / 5

Еще раз. Давайте не использовать expression body для функций, которые возвращают Unit, то есть в ситуации, когда мы хотим записать сайд эффект. Тогда мы привнесем в стайл гайд смысловую нагрузку, упрощающую понимание кода. То, что Kotlin компилирует и то, и другое – это ограничение реализации этого языка. Нам ничто не мешает разделить эти средства выразительности по смыслу применения. Потому что только после внесения смысловой нагрузки окончательно пропадут обсуждения в каком стиле когда писать, в которых до этого момента приводились только количественные метрики (количество строк кода) и эстетика (которая субъективна).

mrblrrd avatar Jan 12 '22 19:01 mrblrrd

Правильно понимаю, что это аргумент в пользу не использовать expression body в принципе, а не к вопросу делать перенос после = или писать в одну строку?

Да, всё так. Я за использование expression-body только в однострочниках. Даже если исключить вероятность ошибки при написании expression-body, всё равно появляются дополнительные вопросы которые нужно дополнительно регулировать:

  • Указывать ли возвращаемый тип при experssion-body. Для явности лучше указывать, но на ревью легко можно пропустить, если его не указали.
  • Писать ли все функции с возвращаемым значением как expression-body? Кажется что только в этом случае логика визуального разделения на функции с сайд-эффектами и с возвращаемым значением заработает.

Согласен, что нужно зафиксировать цели кодстайла, чтобы при принятии решений опираться на них. Сделаем это в отдельном issue?

osipxd avatar Jan 13 '22 08:01 osipxd

Да, всё так.

Ок, мне теперь кажется, я окончательно понял твою позицию и аргументы.

Согласен, что нужно зафиксировать цели кодстайла, чтобы при принятии решений опираться на них. Сделаем это в отдельном issue?

Ок, я постараюсь закинуть issue. Если кто-то раньше сделает, то я обязательно подключусь.

Промежуточный итог

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

Пункт 1

Kotlin позволяет иначе записывать функцию, которая состоит из одного выражения (на следующем шаге определим, что это такое).

fun foo(): Int {     // bad
    return 1
}

fun foo() = 1        // good

Они рекомендуют использовать expression body всегда, когда функция состоит из одного выражения. https://kotlinlang.org/docs/coding-conventions.html#functions

Пункт 2

Отвлечемся от Kotlin и вспомним, что в языке можно выделить конструкции в следующие группы:

  1. Expression (выражения). Это конструкция, которая вычисляет какое-то значение и поэтому имеет тип.
  2. Statement (оператор или инструкция). Это последовательность действий, котороая изменяет общее состояние, т.е. производит какой-то side-effect.

Пункт 3

Вернемся к Kotlin. Проблема в том, что этот язык позволяет записать выражение, которое ничего не возвращает (делает side-effect), т.е. имеет тип Unit, и при этом опустить указание return type. Таким образом, происходит смешение двух понятий из п.2. Однако, в своем Coding Convention они используют именно термин single expression, да и такую запись функции называют expression body. Поэтому у меня есть сомнения, что они действительно одобряют такую запись для Unit-выражений. Но то, что язык это позволяет – факт.

fun foo(): Unit {     // bad
    return Unit
}

fun foo() = Unit        // good

Пункт 4

Я вижу три варианта:

  1. Разрешить expression body для любых выражений, тип которых отличный от Unit. Таком образом, мы закрепим за такой зписью смысл, и она не будет просто синтаксическим сахаром.
  2. Разрешить expression body абсолютно для любых случаев, но обязательно указывать return type Unit для Unit-выражений. Для выражений других типов указание return type – на усмотрение разработчика, хочет ли он указать его, чтобы форсировать, либо в целях документации. В этом случае мы будем использовать все созможности языка, а expression от statement отличать по явному указанию типа Unit.
  3. Полностью запретить expression body. Как говорил известный политический деятель: «Нет expression body, нет проблемы».

Вариант 1 – я за. Вариант 2 – я готов на это пойти. Вариант 3 – мне будет грустно.

Я за использование expression-body только в однострочниках.

Количественные критерии мне не нравятся.

mrblrrd avatar Jan 13 '22 10:01 mrblrrd

Очень всё перемешалось. Но я так понимаю основной поинт этот:

Давайте не использовать expression body для функций, которые возвращают Unit, то есть в ситуации, когда мы хотим записать сайд эффект.

У меня нет к этому негатива, хороший поинт для понимания языка. Но вот надо ли это в стайлгайде не знаю. Пока размышлял об этом возникли такие вопросы/мысли:

👉 Кажется это правило отметёт пожелания некоторых писать = with(binding). 👉 Лично мне проще заложить смысл в имя переменной, чем в конструкцию языка (помнить про это для меня это доп нагрузка. Жили же без этого в Java 😄 ).

❓ Еще хочу спросить про пример с

open fun initViews() = Unit

Добавление типа не исправило бы потенциальную проблему?

open fun initViews(): Unit = Unit

❓ И про Nothing, c ним юзать экспрешн бади тогда или нет?

❓ Что делать если есть Unit функция doWork() и нам надо сделать делегирующую функцию fun delegateWork() = delegate.doWork()... Так будет нельзя написать и придется делать блок бади, хотя такое написание очень удобным кажется.

❓ Не возникает ли доп сложности в рефакторинге? В случае если была функция с значением и использовалась в другой как fun delegateWork() = delegate.doWork(), мы ее изменили так что она перестала возвращать Int. Выведение типов нам никак не подскажет, что надо переделать delegateWork() в блок бади (Только явный тип функции помог бы). А если вызывающий delegateWork() не использовал на самом деле возвращаемое значение (что бывает, например List.remove()), то кажется просто лишней работой.

❓ Вопрос от Осипа:

Писать ли все функции с возвращаемым значением как expression-body? Кажется что только в этом случае логика визуального разделения на функции с сайд-эффектами и с возвращаемым значением заработает.

Меня тоже волнует. Кажется если будут перемешаны возвращающие значение block-body и expression-body функции, то вся логика развалится. То есть я хочу подчеркнуть, что функция что-то возвращает, делаю expression-body, но потом функция становится сложнее и приходится сделать block-body.... То что я подчеркивал пропадает?


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


Проблема визуального путания expression body функции и property никак не решается в предложенных тобой вариантах. Я бы хотел отличать функции, но это вкусовщина, я могу жить с этим.

В остальном: вариант 1 - могу на это пойти, вариант 2 импонирует, он самый простой на мой взгляд, вариант 3 - не хотел бы, для простых штук потеряем краткость.

Jeevuz avatar Jan 13 '22 14:01 Jeevuz

Кажется это правило отметёт пожелания некоторых писать = with(binding).

Я убежден, что так не следует делать. Зачем из кода, который по задумке выполняет сайд эффект (настраивает вью), делать выражение, которое ничего не вычисляет и не возвращает? Это какой-то абьюз экспрешенов, предназначенных для вычисления значений. Поступая таким образом, всегда надо заботиться, чтобы последним выражением в блоке было Unit-выражение. Если там окажется вызов метода, который в виду API, возвращает не Unit, а например, Int, то придется двигать строки. Мне кажется, это уже намекает, что не подхдящая контрукция языка используется для реализации задуманного.

При этом, если мы выберем вариант 2, то вполне сносной будет запить с явным указанием типа Unit, т.е. объявлением, что здесь – экспрешен-сайд-эффект по дизайну:

fun initViews(): Unit = 
    with(binding) { 
        // ...
    }

Проблема никуда не пропадает. Но явно подчеркивает и не маскирует экспрешен с сайд-эффектом.

Жили же без этого в Java 😄

Насколько я знаю, в Java statement – это всегда statement, а expression – это всегда expression. В этом языке нет выражений, которые ничего не вычисляют. Там if else всегда statement, и его нельзя использовать как expression, например, присвоить переменной. И с другой стороны, нельзя сделать expression, который делает сайд эффект, ничего не возвращая, и использовать его в контекст statement. Могу, конечно, уже плохо помнить. Поправьте меня, если ошибаюсь.

❓ Еще хочу спросить про пример с

open fun initViews() = Unit

Добавление типа не исправило бы потенциальную проблему?

open fun initViews(): Unit = Unit

Если пойдем по варианту 2, то исправит.

❓ И про Nothing, c ним юзать экспрешн бади тогда или нет?

Ох, если он нам когда-нибудь понадобится, то я бы использовал правила, как и для Unit. То есть в варианте 1 не использовать для таких методов expression body, а в варианте 2 всегда явно указывать тип.

❓ Что делать если есть Unit функция doWork() и нам надо сделать делегирующую функцию fun delegateWork() = delegate.doWork()... Так будет нельзя написать и придется делать блок бади, хотя такое написание очень удобным кажется.

В варианте 1 будет нельзя. И block body явно подчеркнет, что тут сайд-эффект. В конце концов тут могу процитировать тебя самого: «Жили же без этого в Java 😄». В варианте 2 можно будет, надо лишь указать тип явно: fun delegateWork(): Unit = delegate.doWork(), и сайд-эффект не будет замаскирован под выражение. И явно зафиксирует интенцию. Система типов подскажет, если этот дизайн поломается.

❓ Не возникает ли доп сложности в рефакторинге? В случае если была функция с значением и использовалась в другой как fun delegateWork() = delegate.doWork(), мы ее изменили так что она перестала возвращать Int. Выведение типов нам никак не подскажет, что надо переделать delegateWork() в блок бади (Только явный тип функции помог бы). А если вызывающий delegateWork() не использовал на самом деле возвращаемое значение (что бывает, например List.remove()), то кажется просто лишней работой.

Отчасти поэтому я предлагаю указывать явный тип функции Unit, потому что это фиксирует дизайн. И если дизайн будет поломан, мы об этом узнаем. Что касается последнего, то не использовать возвращаемое значение плохо, это добавляет неявность. К тому же, если ты так написал delegateWork() = List.remove() (ты подразумевал сайд-эффект, и хотел Unit), то теперь ты не застрахован, что кто-то снаружи видит твой Boolean и накрутил на нем логику. Особенно плохо, если ты изменил реализацию метода (не заботясь о контракте, ведь ты относился к методу, как к сайд-эффекту) на другую по смыслу, но в которой типы сошлись delegateWork() = runActionReturnStatus(), fun runActionReturnStatus(): Boolean { /* ... */ }. Теперь накрученная снаружи логика поломана, но мы об этом узнаем не сразу.

❓ Вопрос от Осипа:

Писать ли все функции с возвращаемым значением как expression-body? Кажется что только в этом случае логика визуального разделения на функции с сайд-эффектами и с возвращаемым значением заработает.

Меня тоже волнует. Кажется если будут перемешаны возвращающие значение block-body и expression-body функции, то вся логика развалится. То есть я хочу подчеркнуть, что функция что-то возвращает, делаю expression-body, но потом функция становится сложнее и приходится сделать block-body.... То что я подчеркивал пропадает?

Не пропадает. Я писал выше следующее:

Множество случаев применения block body – надмножество случаев применения expression body.

То есть когда я вижу вот такие варианты записи:

fun doSomeWork(): Unit =

fun doAnotherWork() {
}

То я в обоих случаях одинаково просто понимаю смысл – оба являются сайд-эффектом.

Аналогично и с такой записью:

fun doSomeWork() =

fun doAnotherWork(): String {
}

Оба вычисляют какое-то значение.

Поэтому если кто-то будет всегда писать block body для функций вычисляющих значение, то проблемы нет, это легко воспринимается. Там нельзя опустить тип.

Здесь важно обратить внимание, что изменение типа возвращаемого значения в block body тебя форсит добавить return type. А expression body – нет. Это дает как гибкость, когда в expression body используется настоящее выражение, так и проблемы, когда в нем используется Unit-выражение. Поэтому в варианте 2 для избавления от этой проблемы можно взять за правило всегда декларировать, что там Unit-выражение: fun doSomeWork(): Unit =.

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

Неправда ваша 🙂 Я согласен c этим, вот что я писал:

Предлагаю не маскирвоать expression body под block body и преимущественно всегда делать перенос после равно =. Кроме случаев, когда помещается в одну строку.

И приводил примеры:

fun singleLine() = just.single().line().ofCode().isPerfectlyOk 

fun singleLineWithBreak() = 
    just.single().line().ofCode().isPerfectlyOk 


fun chain() = 
    just
        .single()
        .line()
        .ofCode()
        .isPerfectlyOk 

fun higherOrderFunctionWithLambda() = 
    higherOrderFunctionWithLambda(params) { 
        // ...
    } 

fun expressionWithCurlyBrackets() = 
    when(something) { 
        // ...
    } 

Я только предлагал не маскировать expression body под block body. То есть писать только так:

fun higherOrderFunctionWithLambda() = higherOrderFunctionWithLambda(params) { /* ... */ } 

fun expressionWithCurlyBrackets() = when(something) { /* ... */ } 

fun higherOrderFunctionWithLambda() = 
    higherOrderFunctionWithLambda(params) { 
        // ...
    } 

fun expressionWithCurlyBrackets() = 
    when(something) { 
        // ...
    } 

Но не так:

fun higherOrderFunctionWithLambda() = higherOrderFunctionWithLambda(params) { 
    // ...
} 

fun expressionWithCurlyBrackets() = when(something) { 
    // ...
} 

Проблема визуального путания expression body функции и property никак не решается в предложенных тобой вариантах. Я бы хотел отличать функции, но это вкусовщина, я могу жить с этим.

Проперти от функций отличаются ключевым словом. Предлагаю так их и отличать. Вот что я писал:

То что функция это функция подчеркивает ключевое слово fun 🤔 Мне трудно понять, зачем нужна дополнительная закрывающая скобка для усиления этого ключевого слова. Опущу лишнее для сравнения:

val lastState: State
    // ...

fun renderState(state: State) = 
    // ...

Итого, я вижу у нас общее желание не отказываться от expression body совсем. Осталось выбрать вариант 1 или вариант 2.

mrblrrd avatar Jan 13 '22 17:01 mrblrrd

Проперти от функций отличаются ключевым словом. Предлагаю так их и отличать

Можно, хотя не всем это удобно при чтении кода (включая меня).

Я только предлагал не маскировать expression body под block body.

Но если это перефразировать и посмотреть примеры, то получается "переносить после равно в многострочниках", тк именно так работает форматтер. Почему бы тогда не пойти таким же путем, как и с val vs fun и не отличать по наличию самого =?

Из всего выглядит так, что можно выбрать вариант 2 с добавкой про Nothing и без уточнений про "маскировку". Тогда мы отметем проблемы, но оставим свободу писать как нравится. По-разному немного, но без ущемлений.

Jeevuz avatar Jan 13 '22 18:01 Jeevuz

Почему бы тогда не пойти таким же путем, как и с val vs fun и не отличать по наличию самого =?

Приведи пожалуйста примеры кода между чем и чем выбираем?

mrblrrd avatar Jan 13 '22 18:01 mrblrrd

Просто из наших обсуждений я предлагаю, чтобы оба эти варианта были возможны:

fun expressionFun1() = 
    when(something) { 
        // ...
    } 
// Когда видишь концовку, то по отступу похожа на val но можно увидеть что это функция поднявшись и увидев fun 

// Похожа на block body, но можно увидеть, что это expression по =
fun expressionFun2() = when(something) { 
    // ...
}  

Jeevuz avatar Jan 13 '22 19:01 Jeevuz

То есть будет просто вариант 2 про то, чтобы всегда указывать тип в expression-body если там Unit или Nothing.

Jeevuz avatar Jan 13 '22 19:01 Jeevuz

Я согласен. Тогда с учетом Unit и Nothing фиксируем такое соглашение по форматированию:

fun expressionFun1() = 
    when(something) { 
        // ... Some value.
    } 

fun expressionFun2() = when(something) { 
    // ... Some value.
}  

fun expressionFun3(): Unit = 
    with(something) { 
        // ... Side-effect.
    } 

fun expressionFun4(): Unit = with(something) { 
    // ... Side-effect.
}

fun expressionFun5(): Nothing = 
    when(something) { 
        // ... Never returns.
    } 

fun expressionFun6(): Nothing = when(something) { 
    // ... Never returns.
}

Ждем мнения остальных!

mrblrrd avatar Jan 13 '22 19:01 mrblrrd

И надо еще раз проговорить один момент, чтобы потом не возвращаться. В вариант 2

Разрешить expression body абсолютно для любых случаев, но обязательно указывать return type Unit для Unit-выражений.

я вкладывал, что коль скоро выражение одно, то какое бы оно ни было, допустимо использовать expression body. Во-первых, я считаю это правильно, ведь выражение одно. Во-вторых, мы избавим себя от нагрузки помнить этот доп и выискивать на ревью. Куда важнее видеть явно сайд-эффект там или выражение здорового человека.

fun expressionFunction() =
    if (isSomething) {
        // ...
    } else {
        // ...
    }

С этим ок? Или конкретно этот момент остается открытым к обсуждению?

mrblrrd avatar Jan 13 '22 19:01 mrblrrd

Мне кажется ок. Это вписывается в общую концепцию. Смысл такое ограничивать был только для намеренного мимикрирования под блок-бади. Раз отказались, то такое должно быть разрешено.

Jeevuz avatar Jan 13 '22 21:01 Jeevuz