Questions on bit-field like type

I’m trying to understand the below behaviour (MWE reduced from a larger set of code). I define an alias for UInt8 called CompositeValue with the intention of storing two values of 4 bits in a single UInt8 in such a way that I can use CompositeValue where it makes sense. I realize that CompositeValue is merely a different name for type UInt8 and might not work as I intend, but will come back to that below.

using Printf

# type = 4 high bits, value = 4 low bits
const CompositeValue = UInt8

type(a::CompositeValue) = a >> 4
value(a::CompositeValue) = a & 0x0f

function Base.show(io::IO, a::CompositeValue)
	print(io, "CompositeValue($(type(a)), $(value(a)))")
end

mutable struct S
    n::UInt8		# Type is UInt8, not CompositeValue

    function S(v::UInt8)
    	@printf("in constructor: %s\n", v)
    	return new(v)    	    	
    end
end

@printf("%s\n", 0x04)
@printf("%s\n", S(0x04))

When I run the code above in Julia 1.5.2 the output is

4
in constructor: 4
S(CompositeValue(0, 4))

I can’t figure out why there isn’t a consistent printing of either the literal integer or as a CompositiveValue(x,y) in three cases? Somehow only the printing of the UInt8 in S is influenced by my (re)definition of Base.show for UInt8/CompositeValue, but not the other two.

Second question is whether there is a better way to have a type CompositeValue that consists of two 4-bit values in Julia? In C/C++ I would use a bitfield. I found BitsFields.jl, but that doesn’t seem to have been updated for at least 9 months, making me a bit wary to use it. Any other way to have a custom type that is not a struct for such values? Also, the overloading of printing a UInt8 isn’t particularly nice…

Use a wrapper type, eg

struct CompositeValue
    value::UInt8
end
3 Likes

I kind of dismissed that idea earlier as being somewhat convoluted, but it indeed works out quite nicely.

Any clue as to why the overrridden Base.show() isn’t called in all cases?

I think it’s because @printf calls Printf.print === Base.print:

julia> @macroexpand @printf("%s\n", 0x04)
...
          Printf.print(var"#85#io", var"#84###x#3265")
...

which in turn calls Base.string:

julia> @code_lowered Printf.print(stdout, 0x04)
CodeInfo(
1 ─ %1 = Base.string(n)
│   %2 = Base.print(io, %1)
└──      return %2
)

All the functionality for formatting and printing is a bit messy imho (show, display, repr, print, string, String, Printf, …). Lots of options, redundancy and source of confusion. Then you have also MIME types and IO context (such as :compact => true). I’m not sure why print bypasses show, but apparently it does so with the print(io::IO, n::Unsigned) method.

2 Likes

The documentation says:

print falls back to calling show , so most types should just define show . Define print if your type has a separate “plain” representation. For example, show displays strings with quotes, and print displays strings without quotes.

And @less print(stdout, 0x4) leads us to the following definitions:

show(io::IO, n::Unsigned) = print(io, "0x", string(n, pad = sizeof(n)<<1, base = 16))
print(io::IO, n::Unsigned) = print(io, string(n))

Conclusion: unsigned numbers can be printed decorated with 0x0... or undecorated. The undecorated version is defined with print, and that’s what is used by @printf("%s", ...) so you will have to define print for your type if you want to override this.

2 Likes
julia> show(stdout, 0x4)
0x04
julia> print(stdout, 0x4)
4

I think I’ve learned something today :wink: Thanks!

2 Likes