# Designing a Paths Julep

**URL:** <https://discourse.julialang.org/t/designing-a-paths-julep/124335>\
**Category:** Internals & Design\
**Tags:** proposal, filesystem, path, rfc\
**Created:** [January 1, 2025, 12:37pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335 "2025-01-01T12:37:25Z")\
**Posts on this page:** 20\
**Page:** 2

<div class="post-metadata">

**Author:** ![DNF](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dnf/32/10191_2.png) [@DNF](https://discourse.julialang.org/u/DNF)\
**Post date:** [January 3, 2025, 8:03am UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/21 "2025-01-03T08:03:45Z")

</div>

My thinking is that joining path legs with a path separator is fundamentally a different operation than string concatenation, which is also why it is common to call it “join”.

And, as @kdheepak points out, what should happen with

```julia
p"my/path" * ".ext"

```

Should it produce `p"my/path/.ext"`?

---

<div class="post-metadata">

**Author:** ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)\
**Post date:** [January 3, 2025, 8:10am UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/22 "2025-01-03T08:10:18Z")

</div>

Good point. I think

```julia
# equivalent to joinpath
/(::Path, ::Path) -> Path
/(::Path, ::String) -> Path

## equivalent to string concat (like file extensions)?
# [deleted] *(::Path, ::String) -> Path

# invalid
*(::Path, ::Path) -> error
*(::String, Path) -> error
/(::String, ::Path) -> error
/(::String, ::String) -> error

```

I think making `*` completely invalid for use on `Path` would also be a good option, to avoid ambiguity here [edit: yeah probably just do this].

Also, if relative paths is made a different type from an absolute path (which semantically seems good), you could even make it so that

```julia
# ok
/(::RelativePath, ::RelativePath) -> RelativePath
/(::RelativePath, ::String) -> RelativePath
/(::AbsolutePath, ::RelativePath) -> AbsolutePath
/(::AbsolutePath, ::String) -> AbsolutePath

# invalid
/(::String, ::RelativePath) -> error
/(::String, ::AbsolutePath) -> error
/(::RelativePath, ::AbsolutePath) -> error
/(::AbsolutePath, ::AbsolutePath) -> error

```

Perhaps those could just be fields of `Path` rather than actually distinct types.

* * *

Edit: nvm, removed the `*(Path, String)` - see below.

---

<div class="post-metadata">

**Author:** ![jules](https://avatars.discourse-cdn.com/v4/letter/j/41988e/32.png) [@jules](https://discourse.julialang.org/u/jules)\
**Post date:** [January 3, 2025, 9:48am UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/23 "2025-01-03T09:48:52Z")

</div>

> [@MilesCranmer](#):
>
> ```julia
> /(::RelativePath, ::AbsolutePath) -> error
> /(::AbsolutePath, ::AbsolutePath) -> error
> 
> ```

This is also the behavior I asked for above and it seems like @tecosaur implemented that now (so you can’t ever append an absolute path).

I’m not sure if I like `*(::Path, ::String) -> Path` just because I don’t think it’s likely that you have a path `dir/subdir/filename` to which you then append `.csv`, at least that has never happened to me that I did path + filename and extension in separate places. It’s more likely to do `p"dir/subdir" / "somefile$ext"`.

---

<div class="post-metadata">

**Author:** ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)\
**Post date:** [January 3, 2025, 11:55am UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/24 "2025-01-03T11:55:11Z")

</div>

Good point. Probably just better to throw an error.

---

<div class="post-metadata">

**Author:** ![DNF](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dnf/32/10191_2.png) [@DNF](https://discourse.julialang.org/u/DNF)\
**Post date:** [January 3, 2025, 12:10pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/25 "2025-01-03T12:10:14Z")

</div>

> [@jules](#):
>
> I’m not sure if I like `*(::Path, ::String) -> Path` just because I don’t think it’s likely that you have a path `dir/subdir/filename` to which you then append `.csv`, at least that has never happened to me that I did path + filename and extension in separate places. It’s more likely to do `p"dir/subdir" / “somefile$ext”.

I can imagine wanting to do

```julia
p"my/path/file" .* [".pdf", ".tex"]

```

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [January 3, 2025, 1:43pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/26 "2025-01-03T13:43:21Z")

</div>

I’m tempted to take a leaf from other ecosystems with more path functions, the rather nice CL library I reference in the design doc [fosskers/filepaths: Modern and consistent filepath manipulation for Common Lisp. - Codeberg.org](https://codeberg.org/fosskers/filepaths) has:

```lisp
> (filepaths:with-base #p"/foo/bar/baz.txt" "jack")
#p"/foo/bar/jack.txt"

> (filepaths:with-name #p"/foo/bar/baz.txt" "jack.json")
#p"/foo/bar/jack.json"

> (filepaths:with-parent #p"/foo/bar/baz.json" #p"/zing")
#p"/zing/baz.json"

> (filepaths:with-extension #p"/foo/bar/baz.txt" "json")
#p"/foo/bar/baz.json"

> (filepaths:add-extension #p"/foo/bar/baz.txt" "zip")
#p"/foo/bar/baz.txt.zip"

> (filepaths:drop-extension #p"/foo/bar/baz.json")
#p"/foo/bar/baz"

```

Rust has the subset: `with_file_name`, `with_extension`, `with_added_extension`.

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [January 3, 2025, 1:53pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/27 "2025-01-03T13:53:30Z")

</div>

> [@jules](#):
>
> One other reason I was thinking about more complex “root” objects was the relocatability issues you also mention in your hackmd. For example, something like [RelocatableFolders.jl](https://juliaregistries.github.io/General/packages/redirect_to_repo/RelocatableFolders) provides.

Right. I haven’t implemented `@/...` yet, but something like what RelocatableFolders.jl does and the linked PRs to base is very much within scope — I guess we’ll see what’s requied for that when I (or you/other people! 😀) take a stab at implementing it.

---

<div class="post-metadata">

**Author:** ![mrufsvold](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mrufsvold/32/31600_2.png) [@mrufsvold](https://discourse.julialang.org/u/mrufsvold)\
**Post date:** [January 3, 2025, 2:10pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/28 "2025-01-03T14:10:05Z")

</div>

FWIW, I have this exact use case: writing out a data file with `.parq` and a metadata file of the same name with `.json`. There’s also the case of adding the `.gz` to a file that already has an extension.

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [January 3, 2025, 2:22pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/29 "2025-01-03T14:22:11Z")

</div>

> [@kdheepak](#):
>
> ```julia
> p"/home/$user_tstr" / dir_tpath / p"$(string(name_tcustom))" * ".csv"
> 
> ```

Oh, just on this example. I think you could just do:

```julia-repl
p"/home/$user_tstr/$dir_tpath/$(name_tcustom).csv"

```

I wouldn’t mind it if your example worked though, to my sensibilities at least that looks reasonable to me.

---

<div class="post-metadata">

**Author:** ![kdheepak](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/kdheepak/32/10881_2.png) [@kdheepak](https://discourse.julialang.org/u/kdheepak)\
**Post date:** [January 3, 2025, 2:38pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/30 "2025-01-03T14:38:06Z")

</div>

I had to write code like this yesterday which is why this example comes to mind. Let’s say I saw a PR that looked like this:

```julia
# basename = p"/home/$user_tstr/$dir_tpath/$(name_tcustom)"
# exts = [".csv", ".json"]

function map_over_extensions(basename, exts)
    for ext in exts
        path = basename * ext
        ...
    end
end

```

If `*` were used for both path concats and string concats, and if I saw this in a code review, I think I’d ask for it to be rewritten as:

```julia
function map_over_extensions(basename, exts)
    for ext in exts
        path = p"$basename$ext"
        ...
    end
end

```

But if `/` were used for paths, I’d let the original code slide. Just my 2 cents 🙂

* * *

My preference:

1. `/` as a means to join path
2. not having any infix operator
3. any other operator (`++`, other unicode symbols) to join paths
4. `*` as a means to join path

---

<div class="post-metadata">

**Author:** ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)\
**Post date:** [January 3, 2025, 2:39pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/31 "2025-01-03T14:39:18Z")

</div>

### Here are some examples of infix operator APIs from a few select languages (C++, Python, Haskell, Nim, Ruby).

> **(click me! It's long so I'm wrapping this in a toggle to not pollute the thread with walls of text:)**
>
> **C++** (`std::filesystem`)
> 
> - Join paths: `/`
> - Change file extension: `.replace_extension(...)`
> - Get parent directory: `.parent_path()`
> - Get file name: `.filename()`
> - Get current working directory: `current_path()`
> - Example:
> 
> ```cpp
> #include <filesystem>
> namespace fs = std::filesystem;
> fs::path base = fs::path("dir");
> fs::path path = base / "file.txt";
> auto parent = path.parent_path();
> auto newPath = path.replace_extension(".bak");
> 
> ```
> 
> **Python (`pathlib`)**
> 
> - Join paths: `/` or `.joinpath(...)`
> - Change file extension: `.with_suffix(...)`
> - Get parent directory: `.parent`
> - Get file name: `.name`
> - Get current working directory: `Path.cwd()`
> - Example:
> 
> ```python
> from pathlib import Path
> base = Path('dir')
> path = base / 'file.txt'
> parent = path.parent
> new_path = path.with_suffix('.bak')
> 
> ```
> 
> **Haskell** (`filepath` package)
> 
> - Join paths: `</>`
> - Change file extension: `-<.>`
> - String joining for extensions: `<.>`
> - Get parent directory: `takeDirectory`
> - Get file name: `takeFileName`
> - Get current working directory: `getCurrentDirectory`
> - Example:
> 
> ```haskell
> import System.FilePath
> base = "dir"
> path = base </> "file.txt"
> parent = takeDirectory path
> newPath = path -<.> "bak"
> 
> ```
> 
> **Nim** (built-in)
> 
> - Join paths: `/`
> - Change file extension: `changeFileExt(...)`
> - Get parent directory: `parentDir(...)`
> - Get file name: `extractFileName(...)`
> - Get current working directory: `getCurrentDir()`
> - Example:
> 
> ```nim
> import os
> let base = "dir"
> let path = base / "file.txt"
> let parent = parentDir(path)
> let newPath = changeFileExt(path, ".bak")
> 
> ```
> 
> **Ruby (`Pathname` library)**
> 
> - Join paths: `/`
> - Change file extension: `.sub_ext(...)`
> - Get parent directory: `.parent`
> - Get file name: `.basename`
> - Get current working directory: `Pathname.getwd`
> - Example:
> 
> ```ruby
> require 'pathname'
> base = Pathname.new('dir')
> path = base / 'file.txt'
> parent = path.parent
> new_path = path.sub_ext('.bak')
> 
> ```

I do kind of have an admiration for Haskell’s syntax but it’s probably unrealistic for Julia at this stage. Also would make more sense if `<>` was string concat (which it isn’t even in Haskell… weird).

Also, I continue to be surprised by how clean C++17 looks!

One theme from poking around is that nearly all languages that _do_ have an infix operator use `/` apart from Haskell which uses `</>` instead.

---

<div class="post-metadata">

**Author:** ![tecosaur](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tecosaur/32/23206_2.png) [@tecosaur](https://discourse.julialang.org/u/tecosaur)\
**Post date:** [January 3, 2025, 3:02pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/32 "2025-01-03T15:02:37Z")

</div>

Thanks for that overview Miles! I remain open to `/` myself, I’m just not sure that would be accepted by the core/triage team overall.

As usual, Ruby’s API looks nice, and I’m somehwhat surprised to see that C++ 17 has a path type!

---

<div class="post-metadata">

**Author:** ![mihalybaci](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mihalybaci/32/13528_2.png) [@mihalybaci](https://discourse.julialang.org/u/mihalybaci)\
**Post date:** [January 3, 2025, 3:25pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/33 "2025-01-03T15:25:35Z")

</div>

In looking at the examples using `/` as the path concatenation operator, I’ve decided that I don’t like it. Even though I know that the variables are paths, I can’t stop thinking that something is getting divided. Both `+` and `*` look much more natural to me for joining paths.

For whatever reason, when I use Python’s `pathlib` I’ve gotten into the (possibly bad) habit of joining paths this way:

```julia
base = Path("/")
home = Path(base, "home")

```

just so that its completely unambiguous that I’m working with paths.

I also like the interpolation syntax `p"$base$file"`. Just my 1 cent.

---

<div class="post-metadata">

**Author:** ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)\
**Post date:** [January 3, 2025, 3:48pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/34 "2025-01-03T15:48:50Z")

</div>

One potential alternative is to just have `Paths.jl` define its own `/` function that is _not_ the `Base.:(/)`.

i.e. a user could then do

```julia
new = let / = Paths.:(/)
    base/file
end

```

or if they want it globally you do

```julia
using Paths: /

new = base/file

```

you just keep this as a totally separate function from `Base.:(/)` and then there is no punning.

---

<div class="post-metadata">

**Author:** ![savq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/savq/32/22063_2.png) [@savq](https://discourse.julialang.org/u/savq)\
**Post date:** [January 3, 2025, 4:15pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/35 "2025-01-03T16:15:19Z")

</div>

Re: path concatenation.

The operator `*` is not associative in Julia, which means multiple `*` will not create multiple intermediate strings.

```julia-repl
julia> dump(:("a" * "b" * "c"))
Expr
  head: Symbol call
  args: Array{Any}((4,))
    1: Symbol *
    2: String "a"
    3: String "b"
    4: String "c"

julia> @which "a" * "b" * "c"
*(s1::Union{AbstractChar, AbstractString}, ss::Union{AbstractChar, AbstractString}...)
     @ Base strings/basic.jl:261

```

On the other hand, `/` is left associative. It’s not guaranteed that it’ll not create intermediate values.

```julia
julia> dump(:(a / b / c))
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol /
    2: Expr
      head: Symbol call
      args: Array{Any}((3,))
        1: Symbol /
        2: Symbol a
        3: Symbol b
    3: Symbol c

```

This isn’t to say `*` is better. I just wanted to point out a more subtle trade-off.

Path concatenation doesn’t really need a binary operator. Using a function (or a constructor method as @nsajko and @mihalybaci suggest) would be easier to learn, and harder to misuse.

---

<div class="post-metadata">

**Author:** ![DNF](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dnf/32/10191_2.png) [@DNF](https://discourse.julialang.org/u/DNF)\
**Post date:** [January 3, 2025, 5:11pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/36 "2025-01-03T17:11:48Z")

</div>

> [@mihalybaci](#):
>
> Both `+` and `*` look much more natural to me for joining paths.

But since a path is a kind of string (or closely related), it’s very unfortunate to use the string concatenation operator for something that’s very different from concatenation. Especially since concatenation is something you probably want to do.

IMO, `*` is the worst possible operator for path join, better to use something completely different.

---

<div class="post-metadata">

**Author:** ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)\
**Post date:** [January 3, 2025, 5:33pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/37 "2025-01-03T17:33:46Z")

</div>

Re: associativity, I think the compiler would just optimize this anyways, especially since it’s likely to be only instances of ~2-3 chained `/` at a time

---

<div class="post-metadata">

**Author:** ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)\
**Post date:** [January 3, 2025, 5:36pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/38 "2025-01-03T17:36:33Z")

</div>

I’m not certain the (pedantic) mathematical justification that underpins the choice of `*` for strings would necessarily apply to a path join operation, but I’m not sure. It’s also worth noting that the choice of `Base`’s `*` for strings _also_ lends itself to the thought that `Base`’s `/` might be used for doing the inverse of concatenation. Yes, julia#13411 was decided against, but `/` means divide and therefore a duck-typed `::Any` method might do some wild things and unexpected things with paths.

An available operator with n-ary parsing like `*` and precedence like `+` is `++`. It’s largely [unused in the ecosystem](https://juliahub.com/ui/Search?type=symbols&q=%2B%2B&u=define).

---

<div class="post-metadata">

**Author:** ![MilesCranmer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/milescranmer/32/21070_2.png) [@MilesCranmer](https://discourse.julialang.org/u/MilesCranmer)\
**Post date:** [January 3, 2025, 6:06pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/39 "2025-01-03T18:06:14Z")

</div>

We should probably use `/` as the path operator - it’s the universal convention across languages and formats (even C++, surprisingly) and visually intuitive. The worry about confusion with division seems unrealistic - I can’t conceive of any scenario where you’d accidentally divide a Path by something, much less have it conflict with path separation. While semantic purity is often valuable, I think `/` as a path separator is too universal, and path manipulation too practical, to sacrifice clarity for theoretical purity.

---

<div class="post-metadata">

**Author:** ![mihalybaci](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mihalybaci/32/13528_2.png) [@mihalybaci](https://discourse.julialang.org/u/mihalybaci)\
**Post date:** [January 3, 2025, 7:11pm UTC](https://discourse.julialang.org/t/designing-a-paths-julep/124335/40 "2025-01-03T19:11:23Z")

</div>

> [@savq](#):
>
> Path concatenation doesn’t really need a binary operator.

I think @savq might be onto something with this. I rarely use operators for string/path concatenation and generally prefer `joinpath()` type functions. From a human-readable perspective is

```julia
path = root / directory / file

```

really that much easier to read or type than

```julia
path = joinpath(root, directory, file) # or
path = p"$root$directory$file"

```

?

I do like the idea of a dedicated, batteries-included `Path` type, but I’m not sure if it needs to have a `/` type operator just because other languages do. I mean, if Python jumped off a bridge, should Julia do it to? 😅

One other thing, would we need to add a `\` for Windows paths? `/` is naturally for unix/linux users, but maybe less so for Windows? I (for one) always found it weird typing `/` in Python for Windows paths.

[Previous page](https://discourse.julialang.org/t/designing-a-paths-julep/124335.md?page=1)

[Next page](https://discourse.julialang.org/t/designing-a-paths-julep/124335.md?page=3)
