11
$\begingroup$

For example:

  • Java uses a toString() method, and calls that behind the scenes. If you don't implement one, you inherit it from the base class (Object).
  • JavaScript uses the ToPrimitive abstract operation, which involves @@toPrimitive, "toString", and "valueOf". You can read the spcification here. Again, if you don't implement any of these, you inherit one from Object.
  • Swift uses three different protocols, which you can read about here. If you don't implement any of them, it uses reflection to generate a sensible default.

What are some advantages and disadvantages to these methods, and what other methods exist?

$\endgroup$

6 Answers 6

9
$\begingroup$

Rust's approach: Display and Debug traits

Rust uses the trait system, through the Display and Debug traits, to model types that may be converted to strings, with two possible and well-known uses (user-facing and developer-facing, respectively).

However, it statically ensures that values of a type that doesn't implement the Display trait may not be used to construct a string representation; the same for Debug. Hence, Rust doesn't have a default string representation for user-defined types.

Even though there is not a default string representation, the language offers a macro to derive the Debug implementation using a sensible default that follows the type's definition. However, the Display trait may not be derived, which forces the developer to implement the user-facing string conversion explicitly.


Haskell has a similar mechanism via the Show type class, which may be derived.

$\endgroup$
5
$\begingroup$

Another option is to simply forbid string coercion, as e.g. Python does. This makes it the programmer's responsibility to explicitly convert other values to strings where necessary, but also means there is no uniquely privileged way to convert to a string.

In Python for example, there are the str and repr functions which convert values to strings in different ways (and which can have user-defined behaviour via the __str__ and __repr__ dunder methods), but you can also convert to string through other means, such as by calling some other method defined by the object's class, or using string formatting or f-strings which may offer more options for how the conversion is done, such as f'{x:.02f}'.

$\endgroup$
4
$\begingroup$

APL specifies a default display form for every array (all values are arrays). Dyalog APL augments this in two ways:

  1. Objects (classes, instances, and namespaces) have a built-in method that takes a character array which then overrides the default form. An object can also set its own display form.

          a←⎕NS⍬  ⍝ new namespace
          a  ⍝ will show default display form
    #.[Namespace]
          a.⎕DF'{myThing}'  ⍝ change it
          a
    {myThing}
    

  2. There is a printing event to which one can set a handler that in turn can do anything it wants based on the array that needs to be printed. The system comes with a bunch of ready-made handlers for various customisable effects.

          MyPrinting←{⎕←⌽⍺}  ⍝ output normal display form, but reversed
          ⎕SE.onSessionPrint←'MyPrinting'  ⍝ assign event handler
          'Hello World'
    dlroW olleH
    

$\endgroup$
3
$\begingroup$

C++ ostream/strstream.

Using a C++-style stream approach is the only way I know for a language that allows users to define types that cannot implement interfaces, like myInt256, but allows these types to take part in the conversion API.

Splitting the stream API from the StringBuffer implementation allows moving the StringBuffer implementation to an optional module. The stream API itself must reside in the core part of the standard library if core types should provide implementations for it and extending types from other modules is not allowed.

A property that I consider neither pro or con is that the Stream API can have a state telling inserted entities a desired format.

$\endgroup$
3
$\begingroup$

In Common Lisp, there are different ways of printing an object depending on what you want. Let's define a custom class first:

USER> (defclass abc () ((x :initarg :x :reader abc-x)))
#<STANDARD-CLASS USER::ABC>

The system can print any object, and converting to a string is done by printing to a string stream. Typically you call WRITE-TO-STRING or FORMAT.

DESCRIBE

Describe is intended for debugging. The default mechanism relies on introspection to print its slots.

USER> (describe (make-instance 'abc :x 10))
#<ABC {1007819283}>
  [standard-object]

Slots with :INSTANCE allocation:
  X                              = 10

Users can define their own methods for the DESCRIBE-OBJECT generic function, with the expectation that the string is only read by humans and does not need to be read back.

PRINT-OBJECT

There is a generic function named PRINT-OBJECT. An important effort in Lisp has been made to handle the case where we want the printed representation of a value to be read back (this leads to a simple serialization/deserialization mechanism, along with MAKE-LOAD-FORM).

The default behavior is implementation-dependent but here for example in SBCL the value is printed with the #<> syntax, displaying the class of the value and a unique identifier:

USER> (make-instance 'abc :x 3)
#<ABC {101D6FD663}>

The #<> syntax is specifically used to represent data that cannot be read back by Lisp (e.g. a thread, a process...). If you ask the runtime to print it readably, you have an error:

USER> (write (make-instance 'abc :x 3) :readably t)
; Evaluation aborted on #<PRINT-NOT-READABLE {101D772E23}>.

You can define a custom printer.

USER> (defmethod print-object ((o abc) stream)
        (if *print-readably*
            (format stream "#.~s" `(make-instance 'abc :x ,(abc-x o)))
            (call-next-method)))
#<STANDARD-METHOD COMMON-LISP:PRINT-OBJECT (ABC T) {101FBDEA53}>

Here I specifically handle the *print-readably* special variable, and fall back to the default method with (call-next-method) when that variable is nil.

I emit code that starts with #., the syntax for calling the evaluator at read-time. When I give the resulting string to the Lisp reader, it will allocate a new object of type ABC with the same value for x when reading the string. The slot value x itself is printed readably.

Here below you can see both ways of printing the value, the first one is the result of printing the value to standard-output, followed by the REPL printing the result of the call using the standard (non-readable) way.

USER> (write (make-instance 'abc :x 5) :readably t)
#.(MAKE-INSTANCE 'ABC :X 5) 
#<ABC {101FC00393}>

Coercing to a string is never done automatically, you have to print the object to a string stream:

USER> (with-output-to-string (out)
        (write (list (make-instance 'abc :x 1)
                     (make-instance 'abc :x 2))
               :stream out
               :readably t))
"(#.(MAKE-INSTANCE 'ABC :X 1) #.(MAKE-INSTANCE 'ABC :X 2))"

Reading back from the string:

USER> (read-from-string *)
(#<ABC {10070E8373}> #<ABC {10070E8423}>)
57

If the actual identity of the objects matters, then it is not a good printing method, but in that case you would need to implement your own way of indexing objects and retrieving them later.

$\endgroup$
1
$\begingroup$

C++ uses character-stream operators (<< and >>) for such conversions.

One advantage is that overloads can provide different representations for different kinds of stream (e.g. we might want a debug_stream class to be verbose, whereas a datastore_stream could be compact but must have full round-trip fidelity).

The disadvantage is that you might end up having to implement the overloads yourself, for libraries without the overloads you need for your stream types. I often see code that provides streaming operators for plain-char streams, but not for wchar_t streams, as a common example.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .