ternary

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 19, 2026 License: MIT Imports: 0 Imported by: 0

README

Ternary: Single-line Conditionals in Go

Rationale

Go doesn’t have ternary expressions. Why do we think that’s a problem?

Most programming languages used in industry have a single-line expression that returns one of two alternatives , but not Go. In C-style languages, ternary expressions are common:

condition ? ifTrueValue : ifFalseValue // evaluates to one of the alternatives

For functional languages that support if-then-else, it is usually already an expression and can be written on a single line. Other languages offer in-line conditionals such as Python’s.

Go, being a C-style language, could have included a ternary expression, but the designers chose to reject such a construct. Too much room for abuse, they say. They are welcome to their opinion, of course, but unfortunately it leads to code sprawl and damages readability and therefore comprehensibility.

Package ternary provides a type that imitates the ternary expression, allowing intuitive single-line conditionals. For example, it is useful when creating struct literals:

func NewGizmo(sprocket, thingy string) Gizmo {
    If := ternary.If[string] // factory for a ternary that returns a string

    return Gizmo{
        sprocket: If(sprocket != "").Then(sprocket).Else("default")
        thingy: If(thingy != "").Then(thingy).Else("default")
    }
}

Compared to traditional Go, it’s hard to miss the difference even when the number of fields is small, e.g. only two here:

func NewGizmo(sprocket string, thingy string) Gizmo {
    if sprocket := "" {
        sprocket = "default"
    }
    if thingy := "" {
        thingy = "default"
    }

    return Gizmo{
        sprocket: sprocket,
        thingy: thingy,
    }
}

Notice that the number of lines of code is a multiple of the number of fields in the struct. For traditional Go, that’s four lines (three conditional and a field assignment) per field. That’s a lines-of-code amplification of 4x.

With ternary.If, it scales as one line per field without loss of readability. As the number of fields goes up, the question stops being whether a single-line conditional is worthwhile and more how you can justify doing it the traditional way at all. Structs may carry a dozen or more fields. We couldn’t show such an example in this README because 12 fields would result in 48 lines of code. The example above was, at one point, three fields, but even that ended up being overly verbose for a README.

Also notice the redundancy of the field assignments (e.g. sprocket: sprocket) in the returned literal. This is boilerplate that is not adding to clarity or comprehension, just an extra step in your mental evaluation of the code.

Is a ternary expression possible to misuse, as the Go authors fear? Yes. Is their response to this fear rational and proportional? Considering all of the sharp edges in the rest of Go, no. There are far more serious ways to get in trouble in Go that cannot be avoided without judgment and discretion. In this case, the Go authors have substituted their judgment for our own and removed the possibility for discretion. Sometimes they have bucked language design trends and done so to good effect, but not this time. This is a set of training wheels that actively hurts developers and that we can do without.

Usage

Import:

import "github.com/binaryphile/fluentfp/ternary"

ternary.If[R any], where R is the return type of the expression, is a factory function that creates a struct. It is generic based on the return type of its Else method, which in our example is string but could be any type.

Create the struct by specifying its condition value with If. Next specify its "if true" value with Then and its "if false" value with Else. Else returns the appropriate value based on the condition:

import t "github.com/binaryphile/fluentfp/ternary"

var first = t.If[string](true).Then("first").Else("second") // Else is always required

Notice that the method chain is always fully executed, which is a difference from the short-circuiting behavior of the traditional if-then-else statement. With an if statement, the branch of code not selected is simply ignored. Here, whatever arguments are given to Then and Else are analogous to the branches, but since they are parameters to a method call, they are both always evaluated before being passed into the method.

That means if it is expensive to calculate one or both of the alternative values, you want to defer that execution and not do it all if the condition doesn't call for it. For this purpose, there are Call versions of the Then and Else methods, i.e. ThenCall and ElseCall. When triggered by the condition, each Call calls the no-argument function that it was given to produce a value. If not triggered by the condition, the function is never called and so short-circuits execution.

import t "github.com/binaryphile/fluentfp/ternary"

// ExpensiveNo isn't called
var yes = t.If[string](true).Then("yes").ElseCall(ExpensiveNo)

That's about all there is to it. ternary is a simple tool that can greatly enhance the conciseness and readability of routine code.

Patterns

Status Strings

Convert boolean state to human-readable strings:

status := ternary.If[string](task.IsDone()).Then("complete").Else("in progress")

Default Values with Conditions

Set defaults based on conditions in a single expression:

timeout := ternary.If[int](config.Timeout > 0).Then(config.Timeout).Else(30)

Factory Alias for Repeated Use

When using the same return type multiple times, alias the factory:

func FormatReport(items []Item) string {
    If := ternary.If[string]

    var lines []string
    for _, item := range items {
        lines = append(lines,
            If(item.IsUrgent()).Then("🔴 ").Else("   ") + item.Name,
        )
    }
    return strings.Join(lines, "\n")
}

Documentation

Overview

Package ternary provides a fluent ternary type for Go.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Ternary

type Ternary[R any] struct {
	// contains filtered or unexported fields
}

Ternary is a fluent ternary that offers the methods Then and Else, meant to be called in order. e.g. Ternary[string]{condition: true}.Then("yes").Else("no") returns the string "yes". See the If factory for more eloquent construction.

func If

func If[R any](condition bool) Ternary[R]

If returns a fluent ternary that ultimately yields a return value of type R when the Else method is called. e.g. If[string](true).Then("yes").Else("no") returns the string "yes". Don't be tempted to put in a function call as an argument to Then or Else thinking it won't be called when the condition doesn't match, i.e. don't do this: If[string](true).Then("yes").Else(ExpensiveNo()). ExpensiveNo() was already evaluated before If had a chance to look at it. Instead, use the ThenCall and/or ElseCall methods that take functions as arguments: If[string](true).Then("yes").ElseCall(ExpensiveNo)

ternary.If can read better in your code if you assign it to a variable when instantiating with a concrete type, e.g. var If = ternary.If[string] in the package namespace. As another alternative, the package itself can be dot-imported to good effect sometimes.

func (Ternary[R]) Else

func (i Ternary[R]) Else(e R) R

Else returns the then value if condition is true, otherwise elseValue.

func (Ternary[R]) ElseCall

func (i Ternary[R]) ElseCall(elseFunc func() R) R

ElseCall returns the then value if condition is true, otherwise elseFunc().

func (Ternary[R]) Then

func (i Ternary[R]) Then(t R) Ternary[R]

Then assigns the value returned by Else or ElseCall when condition is true.

func (Ternary[R]) ThenCall

func (i Ternary[R]) ThenCall(thenFunc func() R) Ternary[R]

ThenCall assigns the value returned by Else or ElseCall when condition is true but defers execution.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL