The is_not_empty() method as more clearly alternative for !is_empty()

I like thinking of things in terms of formal mathematics A LOT even though I'm only an amateur/self-taught mathematician (if that word even applies to me). That being said, as a person who has built and maintained large applications my entire career across multiple languages I can tell you that the vast majority of software developers (at least in the business world) find highly mathematical explanations and naming of things to be nothing but a confusing morass. That's why LINQ in C# eschews formal functional/monadic terminology like map/flatMap/fold/filter/reduce and instead uses terminology like select/selectMany/group/where etc.

I know that Rust is intended to be a more formal language, but, I also know that it strives to make "System Programming" more accessible to "more average" developers. With that understanding, I would tend to want to make terminology as clear as possible and even go ahead and use formal mathematical concepts, as long as those concepts are surface understandable without an extensive math background.

Just an opinion.

2 Likes

We have been mostly talking about boolean functions, were I see we could also choose something like the following:

struct Element;

trait HasContents {
    fn has(&self, value: Option<Element>) -> bool {
        if value.is_some() {
            self.len() != 0
        } else {
            self.len() == 0
        }
    }
}

This could then be used either as

vec![].has(Some(Element))

or

vec![].has(None)
2 Likes

That's confusing, because

vec![1].has(Some(2)) == true

We shouldn't need to pass an element into the function, that isn't even used.

1 Like

No I literally meant vec![].has(Some(Element)) since Element is a struct used in the type.

That seems really convoluted for checking if a vector has items in it.

7 Likes

That is very fair but I would argue that it is extremely readable

We could just do vec.has_some_element() for the same readability without the cost of having a whole new type for just this.

2 Likes

Surprised nobody here suggests

unless!(vec.is_empty() {
     do_something_here();
});

or even just

unless vec.is_empty() {
     do_something_here();
}

That has indeed been mentioned already, and most have shown a dislike for unless.

9 Likes

As a non-native English speaker, unless is messing with my mind a lot. IMO it satisfies people that think the code should literally read like a prose/poetry, at the cost of sacrificing obvious, cold readability.

13 Likes

Thank you for sharing that. That's not an insight that native US English speakers are likely to have on their own.

1 Like

Heck, I'm a native English speaker and unless messes with my mind a lot too. I cannot for the life of me mentally parse unless statements. I'm familiar with the language (both English and programming). I can verbally explain how unless statements work (unless x is just if !x). But my brain completely melts when trying to parse unless statements.

I guess what I'm trying to say is you're not alone.

2 Likes

If we don't have any word to express the idea of !is_empty(), maybe we should express it using sigil?

Below are my preliminary excuses for this "yet another sigil" proposal:

  • Sigils anyway works well for many other common ideas (math, references, logic, etc.)
  • !is_empty() is common enough to deserve its own sigil
  • The result would be safe alternative to truthiness concept available in other programming languages

So, I propose prefix + operator as !is_empty() replacement.

It's not overloaded yet, and somehow it resembles non-empty meaning, therefore it could become a viable alternative for the current syntax. Further it could replace various other checks e.g. is_some(), is_positive(), is_ok(), etc. however I'm not sure if we should go so far.


examples taken from the bottom of `rg '.is_empty()' -C 10` execution on `rust-lang/rust` repository

if !rustflags.is_empty() {
    return Ok(rustflags);
}

if +rustflags {
    return Ok(rustflags);
}



// First try RUSTFLAGS from the environment
if let Ok(a) = env::var(name) {
    let args = a.split(' ')
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .map(str::to_string);
    return Ok(args.collect());
}

// First try RUSTFLAGS from the environment
if let Ok(a) = env::var(name) {
    let args = a.split(' ')
        .map(str::trim)
        .filter(|s| +s)
        .map(str::to_string);
    return Ok(args.collect());
}



if !rustdocflags.is_empty() {
    self.compilation
        .rustdocflags
        .entry(unit.pkg.package_id().clone())
        .or_insert(rustdocflags);
}

if +rustdocflags {
    self.compilation
        .rustdocflags
        .entry(unit.pkg.package_id().clone())
        .or_insert(rustdocflags);
}



let feats = self.bcx.resolve.features(unit.pkg.package_id());
if !feats.is_empty() {
    self.compilation
        .cfgs
        .entry(unit.pkg.package_id().clone())
        .or_insert_with(|| {
            feats
                .iter()
                .map(|feat| format!("feature=\"{}\"", feat))
                .collect()
        });
}

let feats = self.bcx.resolve.features(unit.pkg.package_id());
if +feats {
    self.compilation
        .cfgs
        .entry(unit.pkg.package_id().clone())
        .or_insert_with(|| {
            feats
                .iter()
                .map(|feat| format!("feature=\"{}\"", feat))
                .collect()
        });
}



if ret.is_empty() {
    if !unsupported.is_empty() {
        bail!(
            "cannot produce {} for `{}` as the target `{}` \
             does not support these crate types",
            unsupported.join(", "),
            unit.pkg,
            bcx.target_triple()
        )
    }

if !+ret {
    if +unsupported {
        bail!(
            "cannot produce {} for `{}` as the target `{}` \
             does not support these crate types",
            unsupported.join(", "),
            unit.pkg,
            bcx.target_triple()
        )
    }



if !to_add.is_empty() {
    new_deps.push((*unit, to_add));
}

if +to_add {
    new_deps.push((*unit, to_add));
}



assert!(result_vec.is_empty());

assert!(!+result_vec);



fn is_line_of_interest(line: &str) -> bool {
    !line.split_whitespace()
        .filter(|sub_string|
            sub_string.contains("file://") &&
            !sub_string.contains("file:///projects/")
        )
        .collect::<Vec<_>>()
        .is_empty()
}

fn is_line_of_interest(line: &str) -> bool {
    +line
        .split_whitespace()
        .filter(|sub_string|
            sub_string.contains("file://") &&
            !sub_string.contains("file:///projects/")
        )
        .collect::<Vec<_>>();
}



if is_file_of_interest(path) {
    let err_vec = lint_file(path);
    for err in &err_vec {
        match *err {
            LintingError::LineOfInterest(line_num, ref line) =>
                println_stderr!("{}:{}\t{}", path.display(), line_num, line),
            LintingError::UnableToOpenFile =>
                println_stderr!("Unable to open {}.", path.display()),
        }
    }
    !err_vec.is_empty()
} else {
    false
}

if is_file_of_interest(path) {
    let err_vec = lint_file(path);
    for err in &err_vec {
        match *err {
            LintingError::LineOfInterest(line_num, ref line) =>
                println_stderr!("{}:{}\t{}", path.display(), line_num, line),
            LintingError::UnableToOpenFile =>
                println_stderr!("Unable to open {}.", path.display()),
        }
    }
    +err_vec
} else {
    false
}



if line.is_empty() {
    is_in_inline_code = false;
}

if !+line {
    is_in_inline_code = false;
}



match self.0 {
    None => other.is_empty(),
    Some(f) => {
        let other = other.trim();
        for v in f.split(' ') {
            if v == other {
                return true;
            }
        }
        false
    }
}

match self.0 {
    None => !+other,
    Some(f) => {
        let other = other.trim();
        for v in f.split(' ') {
            if v == other {
                return true;
            }
        }
        false
    }
}



impl<'a> PartialEq<&'a str> for CpuInfoField<'a> {
    fn eq(&self, other: &&'a str) -> bool {
        match self.0 {
            None => other.is_empty(),
            Some(f) => f == other.trim(),
        }
    }
}

impl<'a> PartialEq<&'a str> for CpuInfoField<'a> {
    fn eq(&self, other: &&'a str) -> bool {
        match self.0 {
            None => !+other,
            Some(f) => f == other.trim(),
        }
    }
}



&mut |line| {
    if !line.is_empty() {
        Err(internal(&format!(
            "compiler stdout is not empty: `{}`",
            line
        )))
    } else {
        Ok(())
    }
},

&mut |line| {
    if +line {
        Err(internal(&format!(
            "compiler stdout is not empty: `{}`",
            line
        )))
    } else {
        Ok(())
    }
},



// Parse the dep-info into a list of paths
pub fn parse_dep_info(
    pkg: &Package, 
    dep_info: &Path
) -> CargoResult<Option<Vec<PathBuf>>> {
    let data = match paths::read_bytes(dep_info) {
        Ok(data) => data,
        Err(_) => return Ok(None),
    };
    let paths = data.split(|&x| x == 0)
        .filter(|x| !x.is_empty())
        .map(|p| util::bytes2path(p).map(|p| pkg.root().join(p)))
        .collect::<Result<Vec<_>, _>>()?;
    if paths.is_empty() {
        Ok(None)
    } else {
        Ok(Some(paths))
    }
}

// Parse the dep-info into a list of paths
pub fn parse_dep_info(
    pkg: &Package, 
    dep_info: &Path
) -> CargoResult<Option<Vec<PathBuf>>> {
    let data = match paths::read_bytes(dep_info) {
        Ok(data) => data,
        Err(_) => return Ok(None),
    };
    let paths = data.split(|&x| x == 0)
        .filter(|x| +x)
        .map(|p| util::bytes2path(p).map(|p| pkg.root().join(p)))
        .collect::<Result<Vec<_>, _>>()?;
    if +paths {
        Ok(Some(paths))
    } else {
        Ok(None)
    }
}

Some of non-trivial drawbacks of this syntax:

  • It may be surprising that applying + to value results into bool instead of a number
  • In some cases code becomes obscure e.g. when type of value isn't immediately known
  • Using it with negation results into noisy code
  • On Fira Code and probably on other fonts it may look too close to deref syntax
  • Assymmetry with prefix -

However, it becomes better when:

  • You read +x as "is x" and !+x as "no x"
  • You get accustomed to a new meaning of prefix + and not read it as "plus"

Oh, please, for the love of all that is truth, knowledge, justice, and the American Way, please don't do this!

11 Likes

Do you have any better suggestion how to read it consistently everywhere e.g. on snippets like this:

if !+ret {
    if +unsupported {
        ...
    }
    ...
}

?

The problem isn't how to read it, but adding a sigil for something like this. It almost reminds me of truthyness and falsiness in Javascript. That is not a parallel that I would like in Rust. I don't want to see a sigil for is_empty/has_items/is_nonempty/etc.

10 Likes

Some musings of mine:

  • This whole topic seems to correlate very strongly with the mathematical constructs of ∀ and ∃. Where
    if !array.is_empty() { ... } 
    
    is similar in meaning to
    if ∃ i in array { ... }
    

So I could see the following syntax being a possible solution:

IF IDENT (, IDENT) *  IN EXPR<type == slicelike> BLOCK

where BLOCK is only entered if the first n items are present.

Examples:

  1. Check if string is not empty (don't bind anything)
    if _ in x {
        println!("not empty");
    } else {
        println!("empty");
    }
    
  2. Check if array has at least length 4 and bind index 3
    if _, _, _, forth in arr {
        println!("{}", forth);
    } else {
        println!("len < 4");
    }
    
1 Like

So,

let slice: &[_];

// ...

if let [_, _, _, x, ..] = slice {
    println!("{}", x);
}
1 Like

I guess so. Didn't know that was a thing. Though I guess

if let [_, ..] = slice { ... }

is not quite as nice and clear as

if _ in slice { ... }

But it gets the job done. However, I don't know if the slice pattern syntax works with strings (which is_empty does). I also don't know how often strings are checked vs slices.

I have checked on godbolt and if you use s.as_bytes() for a string then the assembly is the same. However, the binding is much less useful since you are binding u8's which are what rust queries when you get the length which is nice, however slightly surprising if the user was expecting `char's

1 Like