Left and right division for strings

For numbers and matrices, we have x * y / y == x and x \ (x * y) == y. Since we have * for strings, we could implement / and \ for strings too:

"abcd" / "cd" == "ab"
"ab" \ "abcd" == "cd"

AFAICT, it would be algebraically sound, useful, and fun. What do you think?

4 Likes

Very fun. What should the behaviour be if one does "ab" \ "cd"? The only reasonable thing seems to be to throw and error, which makes me wonder how much utility thisā€™d really have.

That said, if we also supported division by a Regex, thatā€™d actually make this thing quite a cute and occasionally useful pattern

5 Likes

Well, itā€™s more like integer division though, i.e., you wonā€™t have

("abc" * "de") / "cde" == "abc" * ("de" / "cde")
2 Likes

In what situation would this be used?

3 Likes

Python has str.removeprefix and str.removesuffix which are similar.

Signature: str.removeprefix(self, prefix, /)
Docstring:
Return a str with the given prefix string removed if present.

If the string starts with the prefix string, return string[len(prefix):].
Otherwise, return a copy of the original string.
Signature: str.removesuffix(self, suffix, /)
Docstring:
Return a str with the given suffix string removed if present.

If the string ends with the suffix string and that suffix is not empty,
return string[:-len(suffix)]. Otherwise, return a copy of the original
string.

Haskell has stripPrefix and stripSuffix for List (and String is a List of Char in Haskell).

Except that these donā€™t throw an error if the prefix/suffix arenā€™t present. A more natural analogue in Julia might be to add more optional arguments to chop, e.g. chop(s, prefix="foo", suffix="bar"). Julia already has the functions chopprefix and chopsuffix for this.

8 Likes

I donā€™t work with strings much, but I have found myself in a few situations before where Iā€™ve done things like

if endswith(path, ".jl")
    path[begin:end-3]
end

could instead be

if endswith(path, ".jl")
    path / ".jl"
end

Thatā€™s a really simple example, but Iā€™ve found a few times when writing string macros, that thereā€™s often prefixes or suffixes I know are there that I want to chop off.

chopsuffix(s, ".jl") was added in Julia 1.8.

8 Likes

Sure, but whereā€™s the fun in that?

2 Likes

Note also that this is buggy:

julia> s = "Ī±.jl"
"Ī±.jl"

julia> s[begin:end-3]
ERROR: StringIndexError: invalid index [2], valid nearby indices [1]=>'Ī±', [3]=>'.'

(Itā€™s really tempting to write this, which is a key motivation for a StringIndex type.)

8 Likes

Nice try, but I will not be tempted into that tar-pit of a thread today :sweat_smile:

8 Likes

Like the idea about regex and just checked that regular languages are closed under union, intersection, complement and concatenation. Thus one could indeed define difference as

\(ra::Regex, rb::Regex) = !rb āˆ© ra

with ! and āˆ© being complement and intersection respectively. Not sure if this efficient in general though?
Interestingly, \ is not defined on sets as difference either ā€“ the fallback method is applicable, but fails.

1 Like

Even though regular languages are closed under complementation, unfortunately I donā€™t think PCRE supports complement. How do I turn any regex into an complement of itself without complex hand editing? - Stack Overflow

Was proposed and declined in Julia#13411. Thereā€™s also the consideration that / could alternatively represent path concatenation (also declined in Julia#9488).

8 Likes

I donā€™t like Base./ for path concat, since it has nothing to do with division.

@StefanKarpinski said in string division: "pre"\"prefix" => "fix", "suffix"/"fix" => "suf". by StefanKarpinski Ā· Pull Request #13411 Ā· JuliaLang/julia Ā· GitHub

We should either stop using * for string concatenation or we should ā€œdouble downā€.

I agree with that. `append`/`concat` function Ā· Issue #53040 Ā· JuliaLang/julia Ā· GitHub proposes an alternative concatenation function append.

What do you expect "de" / "cde" to do here?

1 Like

Good question, by analogy with integer division it should be zero, i.e., an absorbing element for *.

1 Like

Just for fun:

import Base: /, \
/(x::String, y::String) = begin s = replace(x, y => "", count=1); s == x ? "" : s; end
\(x::String, y::String) = /(y::String, x::String) 

"abcd" / "cd"       # == "ab"
"ab" \ "abcd"       # == "cd" 
"de" / "cde"        # == ""

Obviously it should be "abdā»Ā¹cā»Ā¹"

5 Likes

Or a Rational{String} typed "ab"//"cd" :stuck_out_tongue_winking_eye:

7 Likes

Iā€™d use splitext for this.

2 Likes