Confused by the various string "factory" functions - shouldn't these play nice together?

I’ve stumbled onto some things that I don’t quite understand - it all started with me trying to get join to generate empty strings "" when joining nothings (Voids).

Between string, String, convert and string generating functions like join, it seems to me that behaviour should be consistent.

Example:

julia> test_arr = [nothing, nothing]
2-element Array{Void,1}:
 nothing
 nothing

julia> join(test_arr)
"nothingnothing"

julia> import Base.string

julia> string(::Void) = ""
string (generic function with 14 methods)

julia> string(nothing)
""

julia> join(test_arr)
"nothingnothing"

julia> convert(String, n::Void) = ""
convert (generic function with 1 method)

julia> join(test_arr)
"nothingnothing"

julia> String(nothing)
------ MethodError --------------------- Stacktrace (most recent call last)

 [1] — String(::Void) at sys.dylib:?

 [2] — String(::Void) at sysimg.jl:53

MethodError: Cannot `convert` an object of type Void to an object of type String
This may have arisen from a call to the constructor String(...),
since type constructors fall back to convert methods.
  1. join does not use my string nor my convert functions - I’m wondering what does it use to convert to strings?
  2. String doesn’t use my convert function either?

What am I missing?

Thanks
A

2 Likes

See doc of string In general you should overload how printing is done

String is totally different. It is an explicit conversion and not printing.

Thanks, I’ll give it a go.

So then, join uses internally one of the print, show, display, etc family of functions?

Hmmm, looking into the docs:

String(s::AbstractString)
Convert a string to a contiguous byte array representation encoded as UTF-8 bytes. This representation is often appropriate for passing strings to C.

Due to the uppercase “S” I expected this to be a string constructor, similar to SomeType(...) - and thus behave pretty much like string. It seems that’s not the case.

@yuyichao

Just to be 100% clear, my issue is not one of printing in the REPL. It is about changing the actual value returned by join([nothing]), from “nothing” to “”.

My use case is closer to this:

julia> [if false
       "moo"
       end] |> join
julia> "nothing"

Yes, though not because of the upper case, it is the constructor.

No. A constructor is basically an explicit constructor. Printing is totally different and that’s what string does as shown in the doc.

That’s not what I’m talking about either. All of the functions you mentioned are wrappers around printing objects and so that’s what you should overload. If you overload one of the wrappers (e.g. string) to not do what the doc says the functions should be doing, it’ll certainly not be consistent with other functions anymore.

And as for the convert. You are overloading (or not overloading at all) the wrong function with the wrong syntax. Use Base.convert(::Type{String}, ::Void). Also note that you should not overload this particular method other than for test purposes.

Thanks very much @yuyichao

You’re right :smile: I don’t know what the heck I was trying to do with that convert! Thanks for pointing that out :blush:

Usually it is a bad idea to change the behavior of a function from another module (here, Base.join) with a type from another module (here, [nothing] is of type Base.Array{Void,1}). It is fine to overload a function from another module, but the rule of thumb is that at least one of the argument types should be specific to your module.

Probably there is a better way to do what you are trying to do. What underlying problem are you trying to solve?

2 Likes

@stevengj Thanks!

I’m building a templating system (think Mustache, but more like Haml). It uses Julia itself. It allows doing things like:

<html_here>
<% if is_logged_in %>
<a href="...">Log off</a>
<% end %>
<more_html_here>

The output of the Julia code is inserted into the page - the problem is, if ! is_logged_in the if will return nothing which ends up as "nothing".

Of course I could force the developer to always do if ... else ... but that’s really ugly.

Or I could define an if() function which in the form if(f::Function, condition::Bool) which would end up as

<% if(is_logged_in) do %>
...
<% end %>

That’s probably acceptable though not equally nice - nor do I like limiting user’s options.

You should just have your own wrapper function that turns nothing into empty string/ignore it instead of overwriting a base function and breaks every other packages.

The normal approach to this in Julia would be to define your own method, and use dispatch to special-case particular types. For example:

templateoutput(io, x) = println(io, x)

templateoutput(io, ::Void) = nothing

function templateoutput(io, X::AbstractVector)
    for x in X
        templateoutput(io, x)
    end
end

Thanks for your help, much appreciated – I ended up defining my own function, no messing around with Base. :sunny: