7

I am using an overloaded version of the StringJoin function since years now, without any problem, as I've invested a lot of time and effort earlier to make its behaviour consistent and predictable (see discussion here). I am fully aware that modifying built-in symbols is not a good idea.

The new StringJoin automatically converts any non-string input to String thus I don't have to type ToString every time. It still threads over lists, i.e. StringJoin[{"1", "2", "3"}] returns "123". Also StringJoin[{1, 2, 3}] returns the same, but StringJoin[{1, 2, 3}, "s"] gives "{1, 2, 3}s". The code is:

toString[expr_String] := expr;
toString[expr_] := ToString@expr;
Unprotect[System`StringJoin];
Attributes[System`StringJoin] = {};
System`StringJoin[expr___] := StringJoin@{expr};
System`StringJoin[expr_List] := 
  Fold[StringInsert[#1, toString@#2, -1] &, "", expr];
Protect[System`StringJoin];

For me, it works as expected, and I am really happy to use it as it saves me a lot of typing. Though there is one minor annoyance I cannot track down. Consider the following example:

Append[test, 1];

Mathematica graphics

Note, that the message is printed with List-s wrapping each argument of the printed StringForm (that is: 1 and Append[test, 1]). Interestingly, StringForm on its own works as expected:

StringForm["Test variable insertion: `1`, `2`.", 111, 222]

Mathematica graphics

Question: Can anyone explain why Message fails to print its result correctly?

Caveat:

Modifying System` symbols could have many unexpected side-effects. It happens for this modified StringJoin too (apart from the above example): as Import uses StringJoin, the modification causes a massive performance drop when importing e.g. images. For details, see this post.

István Zachar
  • 47,032
  • 20
  • 143
  • 291

2 Answers2

13

Short answer: because StringJoin has this little-known behavior normally:

StringJoin["abc", {"def"}, "ghi"]

(* 
  ==> "abcdefghi"
*)

while after you overloaded it, the result is

"abc{def}ghi"

Long answer

You start by using

Trace[Append[test, 1], TraceInternal -> True]

from the Trace output, you can locate the following code:

StringJoin[(If[ListQ[#1], StringJoin["\[NoBreak]", #1, "\[NoBreak]"],  #1] & ) /@ 
   StringSplit[
       "Nonatomic expression expected at position `1` in `2`.", 
        System`Dump`del : "`" ~~ DigitCharacter ... ~~ "`" :> 
           {System`Dump`del}]
]

returning

"Nonatomic expression expected at position \[NoBreak]{`}\[NoBreak] in \[NoBreak]{`}\[NoBreak]."

Analyzing this code leads to the short answer above.

Conclusions

Don't overload built-ins globally. Who knows what else you will break? There are other options to do what you want. For example, you can use Internal`InheritedBlock to create local environments, as explained e.g. here.

Leonid Shifrin
  • 114,335
  • 15
  • 329
  • 420
  • Pardon me, but it is not my fault if I mess up something that is not documented at all (though I will suffer the consequences). Generally I never modify anything that's built-in, but this function proved really useful throughout the years. Of course it's a PITA when you have to deploy packages to others... – István Zachar Feb 06 '12 at 14:44
  • So, Leonid, I guess you highly DO NOT recommend to put a Flatten in the 6th line to read: System`StringJoin[expr_List] := Fold[StringInsert[#1, toString@#2, -1] &, "", Flatten@expr]; – István Zachar Feb 06 '12 at 14:56
  • 4
    @Istvan The problem is that the system itself is using many of its own functions, to implement other functions. And Mathematica as a language does not have a formal specification, and is defined by its single implementation. In practice, I think it is just not possible to anticipate all the consequences of your modifications for such a huge system. Even in your example, the problem happened in a rather unexpected place, and only because you overloaded a single function, you could make a connection. Overload many, and it will go off hand. This is just my opinion, of course. – Leonid Shifrin Feb 06 '12 at 16:17
  • @Istvan Note by the way, that, apart from the above considerations, the way you overload StringJoin on lists is wasteful in two ways: it converts to strings even if an expression is a string already, and (which is much worse), by using Fold, it will have quadratic time and memory complexity in the size of the string list (similar to Append for lists), while the original StringJoin is linear. So, someone (this may be you) used to fast action of StringJoin on lists, will have an unpleasant surprise (perhaps without knowing that StringJoin was changed) - another reason to avoid this. – Leonid Shifrin Feb 06 '12 at 16:36
  • I agree to the performance-drop due to Fold, but the code does not convert strings to strings: toString does nothing when its input is already a string. – István Zachar Feb 06 '12 at 17:01
  • @Istvan Oops, sorry, missed that part. Yes, you are right - I wasn't reading the code carefully. – Leonid Shifrin Feb 06 '12 at 17:03
  • 4
    @IstvánZachar The behavior of StringJoin when given lists is documented in the third Basic Example, even if it's not under More Information. – Brett Champion Feb 06 '12 at 18:46
  • @Istvan Thanks for the accept. I hope my answer did not disappoint you too much regarding the possibilities to use the syntax you like. I very much like the idea, I just think that modifying built-ins directly is a wrong road to take. – Leonid Shifrin Feb 06 '12 at 20:30
  • 1
    @BrettChampion: indeed, I did realize that later. @Leonid: Not at all, since I don't use StringJoin in heavy computation. Moreover, my question was answered perfectly :) – István Zachar Feb 07 '12 at 08:59
10

I agree that a user needs to proceed with caution when modifying System` functions. However, once a long time ago I saw a Wolfram Research tech-support web-page that provided a way to overload a System function to patch a bug. My code below uses the same trick which is very neat!

$modifyStringJoin = True;
wasProtected = Unprotect[StringJoin];

StringJoin[args__] /; $modifyStringJoin := Block[{$modifyStringJoin},
      StringJoin[Map[ToString, Flatten[{args}], Heads -> False]]
      ];

Apply[Protect, wasProtected, Heads -> False];

This version has linear complexity. Also, if you ensure $modifyStringJoin is not True, the code above is disabled.

---- Going off on a tangent -----

Using Apply[Protect, wasProtected] ensures ToString is Protected 'if and only if' it was protected before evaluating the code above.

Notice I use the InputForm of Map, Apply, and I include (Heads->False). I always do this to ensure my code is not brokent if somebody evaluates

   SetOptions[Map, Heads->True];

However, I don't know what system code would be broken if you do that!

Imagine what would happen to the system code if you evaluate

Unprotect[Plus];
Attributes[Plus, {Flat, Listable, NumericFunction, OneIdentity, Orderless}];

or if you overload ReplacePepeated!

István Zachar
  • 47,032
  • 20
  • 143
  • 291
Ted Ersek
  • 7,124
  • 19
  • 41