An alternative to elvis operator

Some languages have elvis operator which allows to coalesce None values like this:

let value = option_a ?: option_b ?: fallback;

There has been proposal to add it in Rust which resolved with summary:

  • Elvis could be extended to work on every Try type
    • Supporting only Option is kind of weak
  • It would be nice if there was a way to access an error value
    • Or rather to report its existence to user
  • People felt that a more general solution is needed
    • Which resulted in recently stabilized let-else expression

But that thread as well as let-else were mostly about having control flow like return, continue on the RHS.

This thread is mostly about providing fallback and introspecting errors (which let-else doesn't allow).

Summary

This alternative to elvis operator covers significantly more use cases.

It "kind of" makes sense in Rust because the syntax remains a bit noisy and perhaps intricate, nevertheless, I've spent a lot of time on it, the result is interesting, and there might be some room for improvement.

Please, treat this as a report of research rather than as a serious proposal.

Syntax

The syntax consists from a special scope which opens with case and closes with `ident without semicolon at the end. This scope contains an expression which must contain a special ~ operator (or many of them).

All together looks more or less like this:

case expression_with~.operators~ `ident

Minimal usage example

// Current Rust
let result = something
    .unwrap_or_else(|e| fallback(e));

// With this syntax
let result = {
    case something~ `e
    fallback(e)
};

*This example isn't very interesting — make sure to review real-world examples provided further.

Mechanics

  • When the case expression executes successfully its resulting value is yielded otherwise error is catched

    • Where:
      • successfully means that it didn't early returned and the ~ operator didn't catched any error

      • yielded means that the case expression value becomes the result of the outer scope

      • catched means that error is bound to backtick identifier and remains available in the current scope

  • The ~ operator is like ? but catches errors forward instead of yeeting them outside

Advanced examples

Below is example from my crate:

// Current Rust
let swayipc::Node { app_id, window_properties, .. } = focused_window;

let focused_application = app_id
    .or_else(|| window_properties
        .and_then(|p| p.instance
            .or(p.class)
            .or(p.title)));

if let Some((x_offset, y_offset)) = offsets
    .get(focused_application
        .as_deref()
        .unwrap_or("none")
    )
{
    x += x_offset;
    y += y_offset;
}
// With this syntax
let swayipc::Node { app_id, window_properties, .. } = focused_window;

let focused_application = {
    case &app_id~ `e1
    case &window_properties~.instance~ `e2
    case &window_properties~.class~ `e3
    case &window_properties~.title~ `e4
    "none"
};

if let Some((x_offset, y_offset)) = offsets
    .get(focused_application)
{
    x += x_offset;
    y += y_offset;
}

And another one:

// Current Rust
fn default_config_path() -> Option<String> {
    use std::path::PathBuf;

    let page_home = std::env::var("XDG_CONFIG_HOME")
        .map(|xdg_config_home| {
            PathBuf::from(xdg_config_home)
                .join("page")
        });

    let page_home = page_home.or_else(|_| std::env::var("HOME")
        .map(|home| {
            PathBuf::from(home)
                .join(".config/page")
        }));

    log::trace!(target: "config", "directory is: {page_home:?}");

    let Ok(page_home) = page_home else {
        return None;
    };

    let init_lua = page_home
        .join("init.lua");
    if init_lua.exists() {
        let p = init_lua.to_string_lossy().to_string();
        log::trace!(target: "config", "use init.lua");
        return Some(p)
    }

    let init_vim = page_home
        .join("init.vim");
    if init_vim.exists() {
        let p = init_vim.to_string_lossy().to_string();
        log::trace!(target: "config", "use init.vim");
        return Some(p)
    }

    None
}
// With this syntax
fn default_config_path() -> Option<String> {
    use std::path::PathBuf;

    let page_home = {
        case PathBuf::from(std::env::var("XDG_CONFIG_HOME")~)
            .join("page") `e1
        case PathBuf::from(std::env::var("HOME")~)
            .join(".config/page") `e2

        return None
    };

    log::trace!(target: "config", "directory is: {page_home:?}");

    let init_lua = page_home
        .join("init.lua");
    if init_lua.exists() {
        let p = init_lua.to_string_lossy().to_string();
        log::trace!(target: "config", "use init.lua");
        return Some(p)
    }

    let init_vim = page_home
        .join("init.vim");
    if init_vim.exists() {
        let p = init_vim.to_string_lossy().to_string();
        log::trace!(target: "config", "use init.vim");
        return Some(p)
    }

    None
}

As we see, it's not only an alternative to elvis but also quite a viable alternative to combinators on Try types without restrictions of closures and extra nesting!

Another observation we can take is that it's fine to have unused error bindings — that's because there's no support for _ which would look quite ugly with this syntax.

Also, compiler will warn when one error binding shadows another, so e1, e2 etc is a mandatory style.

Special case for Option binding

Let take a look at the following example from wluma:

// Current Rust
pub fn new(
    base_path: &str,
    thresholds: HashMap<u64, String>
) -> Result<Self, Box<dyn Error>> {
    Path::new(base_path)
        .read_dir()
        .ok()
        .and_then(|dir| {
            dir.filter_map(|e| e.ok())
                .find(|e| {
                    fs::read_to_string(e.path().join("name"))
                        .unwrap_or_default()
                        .trim()
                        == "als"
                })
                .and_then(|e| {
                    parse_illuminance(e.path())
                        .or_else(|_| parse_intensity(e.path()))
                        .ok()
                })
        })
        .map(|sensor| Self { sensor, thresholds })
        .ok_or_else(|| "No iio device found".into())
}
// With this syntax
pub fn new(
    base_path: &str,
    thresholds: HashMap<u64, String>
) -> Result<Self, Box<dyn Error>> {
    case {
        let path = Path::new(base_path)
            .read_dir()~
            .filter_map(|e| e.ok())
            .find(|e| {
                fs::read_to_string(e.path().join("name"))
                    .unwrap_or_default()
                    .trim()
                    == "als"
            })~;

        let sensor = {
            case parse_illuminance(path.path())~ `e1
            case parse_intensity(path.path())~ `e2
        };

        Ok(Self { sensor, thresholds })
    } `e

    Err("No iio device found").into()
}

Important is the let sensor = {..} part: it demonstrates that when the case parent scope evaluates to () we make the parent scope evaluating to Option as if fallback had been None and every case expression on the same level had been wrapped in Some.

In other words it's a shortcut for making something like this:

let sensor = {
    case Some(parse_illuminance(path.path())~) `e1
    case Some(parse_intensity(path.path())~) `e2
    None
};

We detect that parent scope evaluates to () only by the presence of ; at its end or by the absence of expression after `ident, that said, there's no type-dependent desugaring hence something like drop(e) as fallback will require case expressions to evaluate into ().

As an alternative to try blocks

Something like this:

let result = try {
    try_a?;
    try_b?;
    ok
};

Can easily become:

let result = {
    case {
        try_a~;
        try_b~;
        Ok(ok)
    } `e
    Err(e)
};

It's not very ergonomic but I think it's completely fine considering that usages of this snippet would be relatively rare. Moreover, what is the point in returning Result when we can readily pattern match on it?

let value = {
    case {
        try_a~;
        try_b~;
        ok
    } `e
    process(e)
};
handle(value)

Desugaring

Desugaring seems to be very simple.

Let begin with the following snippet:

let result = {
    case something~ `e
    process(e)
};

First when case appears in some scope then label 'a: is added to that scope:

let result = 'a: {
    case something~ `e
    process(e)
};

Next case expression replaces with regular scope labeled with 'b::

let result = 'a: {
    let e = 'b: { something~ };
    process(e)
};

Then expression inside of that scope becomes prefixed with break 'a:

let result = 'a: {
    let e = 'b: {
        break 'a {
            something~
        }
    };
    process(e)
};

While the ~ operator desugars as ? but with break 'b inside:

let result = 'a: {
    let e = 'b: {
        break 'a {
            match Try::branch(something) {
                ControlFlow::Continue(v) => v,
                ControlFlow::Break(r) => break 'b FromResidual::from_residual(r),
            }
        }
    };
    process(e)
};

Through, there's a bit different desugaring when error binding isn't used:

let result = 'a: {
    'b: {
        break 'a {
            match Try::branch(something) {
                ControlFlow::Continue(v) => v,
                ControlFlow::Break(_ignored) => break 'b,
            }
        }
    };
    process()
};

And lastly, if parent scope results in () then None and Some(_) are added:

let result = 'a: {
    'b: {
        break 'a {
            match Try::branch(something) {
                ControlFlow::Continue(v) => Some(v),
                ControlFlow::Break(_ignored) => break 'b,
            }
        }
    };
    None
};

More real-world examples

From smithay

// Current Rust
fn fullscreen_output_geometry(
    wl_surface: &WlSurface,
    wl_output: Option<&wl_output::WlOutput>,
    space: &mut Space<Window>,
) -> Option<Rectangle<i32, Logical>> {
    // First test if a specific output has been requested
    // if the requested output is not found ignore the request
    wl_output
        .and_then(Output::from_resource)
        .or_else(|| {
            let w = space
                .elements()
                .find(|window| window.toplevel().wl_surface() == wl_surface)
                .cloned();
            w.and_then(|w| space.outputs_for_element(&w).get(0).cloned())
        })
        .and_then(|o| space.output_geometry(&o))
}
// With this syntax
fn fullscreen_output_geometry(
    wl_surface: &WlSurface,
    wl_output: Option<&wl_output::WlOutput>,
    space: &mut Space<Window>,
) -> Option<Rectangle<i32, Logical>> {
    // First test if a specific output has been requested
    // if the requested output is not found ignore the request
    case {
        let o = Output::from_resource(wl_output)~;
        space.output_geometry(&o)~
    } `e1

    case {
        let w = &space
            .elements()
            .find(|window| window.toplevel().wl_surface() == wl_surface)
            .cloned()~;
        let o = space
            .outputs_for_element(w)
            .get(0)
            .cloned()~;
        space.output_geometry(&o)~
    } `e2
}

Another one:

// Current Rust
fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial) {
    let seat: Seat<AnvilState<BackendData>> = Seat::from_resource(&seat).unwrap();
    let kind = PopupKind::Xdg(surface);
    if let Some(root) = find_popup_root_surface(&kind).ok().and_then(|root| {
        self.space
            .elements()
            .find(|w| w.toplevel().wl_surface() == &root)
            .cloned()
            .map(FocusTarget::Window)
            .or_else(|| {
                self.space
                    .outputs()
                    .find_map(|o| {
                        let map = layer_map_for_output(o);
                        map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL).cloned()
                    })
                    .map(FocusTarget::LayerSurface)
            })
    }) {
        let ret = self.popups.grab_popup(root, kind, &seat, serial);

        if let Ok(mut grab) = ret {
            if let Some(keyboard) = seat.get_keyboard() {
                if keyboard.is_grabbed()
                    && !(keyboard.has_grab(serial)
                        || keyboard.has_grab(grab.previous_serial().unwrap_or(serial)))
                {
                    grab.ungrab(PopupUngrabStrategy::All);
                    return;
                }
                keyboard.set_focus(self, grab.current_grab(), serial);
                keyboard.set_grab(PopupKeyboardGrab::new(&grab), serial);
            }
            if let Some(pointer) = seat.get_pointer() {
                if pointer.is_grabbed()
                    && !(pointer.has_grab(serial)
                        || pointer.has_grab(grab.previous_serial().unwrap_or_else(|| grab.serial())))
                {
                    grab.ungrab(PopupUngrabStrategy::All);
                    return;
                }
                pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
            }
        }
    }
}
// With this syntax
fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial) {
    let seat: Seat<AnvilState<BackendData>> = Seat::from_resource(&seat).unwrap();
    let kind = PopupKind::Xdg(surface);

    let focus_target = {
        case {
            let root = find_popup_root_surface(&kind)~;
            let el = self.space.elements()
                .find(|w| w.toplevel().wl_surface() == &root)
                .cloned()~;
            FocusTarget::Window(el)
        } `e1
        case {
            let outp = self.space.outputs()
                .find_map(|o| {
                    let map = layer_map_for_output(o);
                    map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
                        .cloned()
                })~;
            FocusTarget::LayerSurface(outp)
        } `e2
    };

    if let Some(root) = focus_target {
        let ret = self.popups.grab_popup(root, kind, &seat, serial);

        if let Ok(mut grab) = ret {
            if let Some(keyboard) = seat.get_keyboard() {
                if keyboard.is_grabbed()
                    && !(keyboard.has_grab(serial)
                        || keyboard.has_grab({
                            case grab.previous_serial())~ `e
                            serial
                        }))
                {
                    grab.ungrab(PopupUngrabStrategy::All);
                    return;
                }
                keyboard.set_focus(self, grab.current_grab(), serial);
                keyboard.set_grab(PopupKeyboardGrab::new(&grab), serial);
            }
            if let Some(pointer) = seat.get_pointer() {
                if pointer.is_grabbed()
                    && !(pointer.has_grab(serial)
                        || pointer.has_grab({
                            case grab.previous_serial()~ `e
                            grab.serial()
                        }))
                {
                    grab.ungrab(PopupUngrabStrategy::All);
                    return;
                }
                pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
            }
        }
    }
}

From alacritty

// Current Rust
pub fn load(options: &Options) -> UiConfig {
    let config_options = options.config_options.clone();
    let config_path = options.config_file.clone().or_else(installed_config);

    // Load the config using the following fallback behavior:
    //  - Config path + CLI overrides
    //  - CLI overrides
    //  - Default
    let mut config = config_path
        .as_ref()
        .and_then(|config_path| load_from(config_path, config_options.clone()).ok())
        .unwrap_or_else(|| {
            let mut config = UiConfig::deserialize(config_options).unwrap_or_default();
            match config_path {
                Some(config_path) => config.config_paths.push(config_path),
                None => info!(target: LOG_TARGET_CONFIG, "No config file found; using default"),
            }
            config
        });

    after_loading(&mut config, options);

    config
}
// With this syntax
pub fn load(options: &Options) -> UiConfig {
    let config_options = options.config_options.clone();
    let config_path = {
        case options.config_file.clone()~ `e1
        case installed_config()~ `e2
    };

    // Load the config using the following fallback behavior:
    //  - Config path + CLI overrides
    //  - CLI overrides
    //  - Default
    let mut config = {
        case load_from(config_path.as_ref()~, config_options.clone())~ `e
        let mut config = UiConfig::deserialize(config_options).unwrap_or_default();
        match config_path {
            Some(config_path) => config.config_paths.push(config_path),
            None => info!(target: LOG_TARGET_CONFIG, "No config file found; using default"),
        }
        config
    };

    after_loading(&mut config, options);

    config
}

Another one:

// Current Rust
#[cfg(not(windows))]
fn installed_config() -> Option<PathBuf> {
    // Try using XDG location by default.
    xdg::BaseDirectories::with_prefix("alacritty")
        .ok()
        .and_then(|xdg| xdg.find_config_file("alacritty.yml"))
        .or_else(|| {
            xdg::BaseDirectories::new()
                .ok()
                .and_then(|fallback| fallback.find_config_file("alacritty.yml"))
        })
        .or_else(|| {
            if let Ok(home) = env::var("HOME") {
                // Fallback path: ~HOME/.config/alacritty/alacritty.yml.
                let fallback = PathBuf::from(&home).join(".config/alacritty/alacritty.yml");
                if fallback.exists() {
                    return Some(fallback);
                }
                // Fallback path: ~HOME/.alacritty.yml.
                let fallback = PathBuf::from(&home).join(".alacritty.yml");
                if fallback.exists() {
                    return Some(fallback);
                }
            }
            None
        })
}
// With this syntax
#[cfg(not(windows))]
fn installed_config() -> Option<PathBuf> {
    // Try using XDG location by default.
    case xdg::BaseDirectories::with_prefix("alacritty")~
        .find_config_file("alacritty.yml")~ `e1
    case xdg::BaseDirectories::new()~
        .find_config_file("alacritty.yml")~ `e2

    let home = env::var("HOME").ok()?;

    // Fallback path: ~HOME/.config/alacritty/alacritty.yml.
    let fallback = PathBuf::from(&home)
        .join(".config/alacritty/alacritty.yml");
    case fallback.exists()
        .then_some(fallback)~ `e3

    // Fallback path: ~HOME/.alacritty.yml.
    let fallback = PathBuf::from(&home)
        .join(".alacritty.yml");
    case fallback.exists()
        .then_some(fallback)~ `e4
}

And yet another one:

// Current Rust
fn commit_hash() -> Option<String> {
    Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .output()
        .ok()
        .filter(|output| output.status.success())
        .and_then(|output| String::from_utf8(output.stdout).ok())
        .map(|hash| hash.trim().into())
}
// With this syntax
fn commit_hash() -> Option<String> {
    case {
        let output = Command::new("git")
            .args(["rev-parse", "--short", "HEAD"])
            .output()~;
        output.status.success()
            .then(())~;
        String::from_utf8(output.stdout)
            .trim()
    } `e
}

From druid

// Current Rust
pub extern "C" fn set_marked_text(
    this: &mut Object,
    _: Sel,
    text: id,
    selected_range: NSRange,
    replacement_range: NSRange,
) {
    with_edit_lock_from_window(this, true, |mut edit_lock| {
        let mut composition_range = edit_lock.composition_range().unwrap_or_else(|| {
            // no existing composition range? default to replacement range, interpreted in absolute coordinates
            // undocumented by apple, see
            // https://github.com/yvt/Stella2/blob/076fb6ee2294fcd1c56ed04dd2f4644bf456e947/tcw3/pal/src/macos/window.rs#L1144-L1146
            decode_nsrange(&*edit_lock, &replacement_range, 0).unwrap_or_else(|| {
                // no replacement range either? apparently we default to the selection in this case
                edit_lock.selection().range()
            })
        });

        let replace_range_offset = edit_lock
            .composition_range()
            .map(|range| range.start)
            .unwrap_or(0);

        let replace_range = decode_nsrange(&*edit_lock, &replacement_range, replace_range_offset)
            .unwrap_or_else(|| {
                // default replacement range is already-exsiting composition range
                // undocumented by apple, see
                // https://github.com/yvt/Stella2/blob/076fb6ee2294fcd1c56ed04dd2f4644bf456e947/tcw3/pal/src/macos/window.rs#L1124-L1125
                composition_range.clone()
            });

        let text_string = parse_attributed_string(&text);
        edit_lock.replace_range(replace_range.clone(), text_string);

        // Update the composition range
        composition_range.end -= replace_range.len();
        composition_range.end += text_string.len();
        if composition_range.is_empty() {
            edit_lock.set_composition_range(None);
        } else {
            edit_lock.set_composition_range(Some(composition_range));
        };

        // Update the selection
        if let Some(selection_range) =
            decode_nsrange(&*edit_lock, &selected_range, replace_range.start)
        {
            // preserve ordering of anchor and active
            let existing_selection = edit_lock.selection();
            let new_selection = if existing_selection.anchor < existing_selection.active {
                Selection::new(selection_range.start, selection_range.end)
            } else {
                Selection::new(selection_range.end, selection_range.start)
            };
            edit_lock.set_selection(new_selection);
        }
    });
}
// With this syntax
pub extern "C" fn set_marked_text(
    this: &mut Object,
    _: Sel,
    text: id,
    selected_range: NSRange,
    replacement_range: NSRange,
) {
    with_edit_lock_from_window(this, true, |mut edit_lock| {
        let mut composition_range = {
            case edit_lock.composition_range()~ `e1
            // no existing composition range? default to replacement range, interpreted in absolute coordinates
            // undocumented by apple, see
            // https://github.com/yvt/Stella2/blob/076fb6ee2294fcd1c56ed04dd2f4644bf456e947/tcw3/pal/src/macos/window.rs#L1144-L1146
            case decode_nsrange(&*edit_lock, &replacement_range, 0)~ `e2
            // no replacement range either? apparently we default to the selection in this case
            edit_lock.selection().range()
        });

        let replace_range_offset = {
            case edit_lock.composition_range()~.start `e 0
        };

        let replace_range = {
            case decode_nsrange(&*edit_lock, &replacement_range, replace_range_offset)~ `e
            // default replacement range is already-exsiting composition range
            // undocumented by apple, see
            // https://github.com/yvt/Stella2/blob/076fb6ee2294fcd1c56ed04dd2f4644bf456e947/tcw3/pal/src/macos/window.rs#L1124-L1125
            composition_range.clone()
        };

        let text_string = parse_attributed_string(&text);
        edit_lock.replace_range(replace_range.clone(), text_string);

        // Update the composition range
        composition_range.end -= replace_range.len();
        composition_range.end += text_string.len();
        if composition_range.is_empty() {
            edit_lock.set_composition_range(None);
        } else {
            edit_lock.set_composition_range(Some(composition_range));
        };

        // Update the selection
        if let Some(selection_range) =
            decode_nsrange(&*edit_lock, &selected_range, replace_range.start)
        {
            // preserve ordering of anchor and active
            let existing_selection = edit_lock.selection();
            let new_selection = if existing_selection.anchor < existing_selection.active {
                Selection::new(selection_range.start, selection_range.end)
            } else {
                Selection::new(selection_range.end, selection_range.start)
            };
            edit_lock.set_selection(new_selection);
        }
    });
}

Drawbacks

  • This syntax is complicated and unusual
    • But as well seems to be useful
    • And doesn't seem to be more complicated than e.g. try-catch in other languages
  • It consists from a new keyword and two operators
    • Perhaps we can reuse ? (questionmark)
    • And maybe it's possible to find another way to bind error
  • It significantly complicates the language
    • However, I believe learning curve of this syntax is lower than learning curve of combinators (it's like async/.await but for Try)
    • Also, it allows some things that are hard to achieve with combinators
  • It's quite noisy
    • But combinators might be noisy and unreadable as well
    • Perhaps custom fonts and proper syntax highlighting might help a bit
  • It allows to write bad code easily
    • Perhaps some lints that prevents bool.then(())~ could fix that
  • Combinators on Result/Option would be soft-deprecated
  • Due to large amount of code already written users would still be required to learn combinators in depth
  • No familiarity with ?:

Note that alternative with reuse of ? operator as well looks viable:

let swayipc::Node { app_id, window_properties, .. } = focused_window;

let focused_application = {
    case &app_id? `e1
    case &window_properties?.instance? `e2
    case &window_properties?.class? `e3
    case &window_properties?.title? `e4
    "none"
};

if let Some((x_offset, y_offset)) = offsets
    .get(focused_application)
{
    x += x_offset;
    y += y_offset;
}

pub fn new(
    base_path: &str,
    thresholds: HashMap<u64, String>
) -> Result<Self, Box<dyn Error>> {
    case {
        let path = Path::new(base_path)
            .read_dir()?
            .filter_map(|e| e.ok())
            .find(|e| {
                fs::read_to_string(e.path().join("name"))
                    .unwrap_or_default()
                    .trim()
                    == "als"
            })?;

        let sensor = {
            case parse_illuminance(path.path())? `e1
            case parse_intensity(path.path())? `e2
        };

        Ok(Self { sensor, thresholds })
    } `e

    Err("No iio device found").into()
}

#[cfg(not(windows))]
fn installed_config() -> Option<PathBuf> {
    // Try using XDG location by default.
    case xdg::BaseDirectories::with_prefix("alacritty")?
        .find_config_file("alacritty.yml")? `e1
    case xdg::BaseDirectories::new()?
        .find_config_file("alacritty.yml")? `e2

    let Ok(home) = env::var("HOME") else {
        return None
    }

    // Fallback path: ~HOME/.config/alacritty/alacritty.yml.
    let fallback = PathBuf::from(&home)
        .join(".config/alacritty/alacritty.yml");
    case fallback.exists()
        .then_some(fallback)? `e3

    // Fallback path: ~HOME/.alacritty.yml.
    let fallback = PathBuf::from(&home)
        .join(".alacritty.yml");
    case fallback.exists()
        .then_some(fallback)? `e4
}

fn commit_hash() -> Option<String> {
    case {
        let output = Command::new("git")
            .args(["rev-parse", "--short", "HEAD"])
            .output()?;
        output.status.success()
            .then(())?;
        String::from_utf8(output.stdout)
            .trim()
    } `e
}

But here are two problems:

  • The ? operator becomes context dependent
  • It's no longer possible to bubble errors from case scope

Could you explain what exactly this is trying to do? I'm not sure what your goal here is.

I'd definitely like the idea of a variant of ? that returns on error rather than success. It'd be super useful when dealing with some complex error handling situations where most the logic is actually in the fallback.

That's basically syntax for cathcing errors and providing fallback values!

Perhaps you aren't familiar with elvis operator, so a better analogy might be try-catch construct which all mainstream languages have.

Currently we rely on .unwrap_or, .unwrap_or_else, .map_or_else etc. to do the job. But the problem with combinators is that they involves some functional programming which isn't easy and also isn't a primary paradigm in Rust.

Moreover, there's a lot of complexity involved in Ok->Err/Err->Ok conversions, unwrap/ok/or/else jargon and in usage of closures.

Instead I want to provide a simple mechanism which allows to ergonomically handle errors without altering imperative code too much like combinators does.

So, that's it. The `e syntax returns the error while success value becomes the result of the outer scope.

I'm familiar with the operator - JS has similar with its ?? operator.

I just wasn't sure how your proposal is trying to implement it.

Here's comparison in case someone else didn't get it:

let focused_application = {
    case &app_id? `e1
    case &window_properties?.instance? `e2
    case &window_properties?.class? `e3
    case &window_properties?.title? `e4
    "none"
};
const focused_application =
    app_id ??
    window_properties?.instance ??
    window_properties?.class ??
    window_properties?.title ??
    "none";

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.