24
$\begingroup$

(For this question, I'm referring to integers. This question also assumes the language still uses base two internally, even if to the programmer it appears as base ten - this question is about syntax).

Numbers can be in different bases. For example, we typically use base ten. But, there are many others. As one example, the digits 10 in binary represent the same number as the digit 2 in decimal. For another example, the digits FF in hexadecimal represent the same number as 255 in decimal.

While most programming languages have numbers 'inputted' as decimal by default, there are some cases where easily allowing other bases can be useful, such as hex.

What are the benefits to having some type of syntax like 0b10 == 2 vs baseSomethingToInt(2, "10") == 2?

Adding dedicated syntax can add complexity, but there are examples of where it could also be useful. What are the reasons to (or not to) have syntax for this?

$\endgroup$
8
  • 6
    $\begingroup$ Why would you want to do this? All numbers on a computer are typically stored in base-2 in one way or another (sometimes in bigger blocks, e.g., base-256 for bigints, which is just 8 base-2 digits together per digit). Actually storing numbers in decimal hasn't been a thing since the 20th century (en.wikipedia.org/wiki/Binary-coded_decimal) $\endgroup$ May 17, 2023 at 2:28
  • 5
    $\begingroup$ I'm personally really frustrated that almost no languages allow floats in different bases, especially since base 2 floats can actually be stored so give accurate answers after most basic operation. $\endgroup$
    – mousetail
    Jun 13, 2023 at 12:06
  • $\begingroup$ just imagine doing something like this in a language like Verilog. An average code will easily blow up 2-3 times bigger and utterly unreadable. $\endgroup$
    – SK-logic
    Jun 14, 2023 at 10:03
  • $\begingroup$ @JörgWMittag The question has been significantly clarified since I made that comment, it originally read more like "what are the pros and cons of allowing numbers to be stored in different bases". Sorry for any confusion. $\endgroup$ Jun 29, 2023 at 16:30
  • $\begingroup$ There's a middle ground, where the language is sufficiently extensible that such representations can be added by user code (or libraries). $\endgroup$ Jul 19, 2023 at 15:25

10 Answers 10

29
$\begingroup$

One advantage is bitwise arithmetic. Bitwise arithmetic is much easier to understand in bases with a power of 2 radix such as hexadecimal. Here it is fairly clear that I am masking out every other byte in these numbers:

uint64_t bswap(const uint64_t n) {
    return (n&0xFF00FF00FF00FF)<<8|(n&0xFF00FF00FF00FF00)>>8;
}

In decimal this is not clear as these numbers seem arbitrary and meaningless:

uint64_t bswap(const uint64_t n) {
    return (n&71777214294589695)<<8|(n&18374966859414961920)>>8;
}
$\endgroup$
19
$\begingroup$

In a domain where a specific base is used, it is clearer if the code can reflect this and still be efficient.

For example, airplane transponders use base 8, and colours are often represented in base 16. It is immediately obvious to an aviation expert that 8b7500 refers to a hijacking, while 3904 not so much. And a designer will know that 16bFFFF00 is yellow, but not so with 11858400000.

$\endgroup$
9
$\begingroup$

The other answers so far have exclusively focused on advantages. There are two disadvantages I can think of, though neither is really a strong enough reason to exclude this feature from a language.

The first disadvantage is particular to the C-style syntax for octal numbers, that is that integer literals beginning with a 0 are treated as octal (base 8). So for example, 012 is not equal to 12. This can be confusing and lead to mistakes; there are some real-world contexts in which it is normal to write decimal numbers with leading zeroes, particularly where this allows numbers to have a fixed number of digits and be sorted lexicographically.

The second disadvantage is that it adds a complication to the standard library's functions for parsing strings as numbers. Particularly, if there is more than one syntax for a number literal in the language, then the user also needs to think about whether or not those different syntaxes are handled by the parse function. In the worst case, the parse function handles other syntaxes and the user may not even be aware that those other syntaxes exist, causing incorrect results when the input string happens to match one of the other syntaxes.

An example is the parseInt function in older versions of Javascript, where parseInt('08') was equal to 0, not 8, because this string is in octal notation ─ but the string is likely to come from an end-user who has no idea what octal is, and who thinks 08 is a perfectly sensible way of writing the number 8. Empirically, this tripped up quite a few people, based on the number of votes and duplicates of this Stack Overflow question.

$\endgroup$
8
  • 3
    $\begingroup$ This is exactly why Python 3 moved to a 0o prefix for octal. $\endgroup$ Jul 27, 2023 at 20:47
  • 1
    $\begingroup$ In most cases where code would want to perform string-to-number conversions, the code will know exactly what format of number is expected, and input in any format would be erroneous. The leading zero in Javascript I find a particularly galling choice. Allowing octal numbers to be represented via syntax like 0q123 or 0o123 or 8x123 would be useful, but allowing the specific syntax 0123 for octal notation is far less useful than allowing and ignoring leading zeroes for general numeric input. $\endgroup$
    – supercat
    Nov 10, 2023 at 18:38
  • $\begingroup$ I don't understand why parseInt is mentioned, when it's got nothing to do with language syntax. $\endgroup$ Nov 13, 2023 at 15:45
  • $\begingroup$ @MatthieuM. Because as mentioned in the answer, Javascript's parseInt function used to parse any valid Javascript integer literal. This demonstrates that the programmer has an extra thing to think about when calling a standard library function to parse an integer, as explained in the previous paragraph. $\endgroup$
    – kaya3
    Nov 13, 2023 at 18:30
  • $\begingroup$ @MatthieuM. The question isn't clear about whether it's talking about literals in the source code or parsing numbers from user input or text files. $\endgroup$
    – Barmar
    Nov 13, 2023 at 21:47
7
$\begingroup$

Runtime safety

With syntax like 0x1A50 instead of parseInt("1A50",16) you can make sure at compile time that 1A50 is a valid hexadecimal.

Sort of a non-issue since you'd expect a programmer to write correct constants into his code, but I did have a classmate who once wrote something like parseInt("J1424", 16) into his code for some test and was then confused that it threw an exception. Having it fail to compile would probably have made the issue far more apparent.

$\endgroup$
2
  • 2
    $\begingroup$ And executing parseInt("1A50",16) at runtime is also much more bloated and less efficient. $\endgroup$
    – CPlus
    Sep 9, 2023 at 20:52
  • 1
    $\begingroup$ @user16217248 An optimizing compiler with knowledge of the function could do that at compile time but yeah, that's also a concern $\endgroup$
    – kouta-kun
    Sep 10, 2023 at 13:43
4
$\begingroup$

Another advantage is in Code Golf (Where many early users of this site came from), in which you try to make the shortest code possible to complete a task. Representing numbers in a higher base can help save a few bytes. I would personally recommend adding a base 256 builtin (Note: base 255 might be better), as each byte can have 256 possible states. This way, you would utilize the byte to its full potential.

$\endgroup$
8
  • 10
    $\begingroup$ "Where most users of this site come from" - citation needed. $\endgroup$ Jul 19, 2023 at 15:25
  • 1
    $\begingroup$ "I would personally recommend adding a base 256 builtin, as each byte can have 256 possible states" - this is a highly impractical proposition. It would require quoting and quote-escaping mechanisms and introduce parsing complexity. The best you could do, I think, is some single-character prefix for byte-sequence literals. A base85 type scheme should be workable, however. $\endgroup$ Jul 27, 2023 at 20:45
  • $\begingroup$ @KarlKnechtel fixed? $\endgroup$ Jul 28, 2023 at 7:32
  • $\begingroup$ @TobySpeight That was written in May, the very early days of the site, when a large proportion of the users here were from Code Golf. It's no longer true, of course. $\endgroup$
    – kaya3
    Nov 10, 2023 at 18:55
  • $\begingroup$ What are the digits in base 256? Do we need to extend to Hebrew, Arabic, and Cyrillic letters? $\endgroup$
    – Barmar
    Nov 13, 2023 at 21:49
3
$\begingroup$

A lot of historical or external constants

Modern languages add this feature because it makes the code a lot cleaner when re-implementing old code or when implementing industry standards.

Think in all the error this functionality avoids, because constants then can be copied instead of the meatbag moist computers doing moist conversions at source code typing.

Also, it's a very easy feature to do, comparable to other language syntax, as any identifier starting with 0 to 9 is already reserved to numbers, so a prefix in the form 0x 0o 0b is a number anyways.

Internal _ number separators help, and type suffixes (like u8) sweetens the deal, as it eliminates a lot of casting.

For an direct example of this, NaNs does not compare with anything, but is encoded with very definitive bit patterns:

def isnan(num f32)
{
    var mask = 0b_0111_1111_1000_0000_0000_0000_0000_0000;
    return num & mask == mask;
}
$\endgroup$
2
$\begingroup$

There are sometimes situations where you want to determine 100% what a constant means. For example, someone implements the sine function, and ends up with a polynomial whose coefficients need to be known precisely. Say there is a coefficient 1.0 / 6.0 and you write it as 0.1666666666666666667. It's now up to the quality of the compiler which number you actually get. There is no floating point number exactly equal to this constant, so it's rounded up or down. And different compilers could round it in different ways. Now if you want a very precise implementation, then further constants along the line will take into account that this first constant wasn't 1/6 but a tiny bit more or a tiny bit less. So the total error in the polynomial can be kept a lot lower than 1 unit of the last floating-point bit.

So you want a notation where you can write down the exact constant that you expect. In C and C++ you use hexadecimal floating point numbers for this purpose.

$\endgroup$
0
$\begingroup$

A numeric value may actually be more kind of the structure, a binary concatenation of something that can be envisioned as fields. Depending on the base of representation, the fields could be easily visible in the written number (that is obviously more readable) or fused together so that computations are needed to extract them. In the binary system, a group of bits of any length can be visually partitioned out. Hexadecimal system allows to see the bit groups taking a multiply of 4 bits (8, 12, 16) while octal systems naturally partitions to multiplies of 3 bits (3, 6, 9). Decimal system does not naturally separate any bit groups.

For instance, a RGB colour contains 3 bytes representing red, green and blue. These components can be visually seen separately if representing the colour in hexadecimal numeric system but not in decimal. IP address can be written as a single long integer, but it represents 4 meaningful groups 8 bits each, so 01010102 (1.1.1.2) in hex makes much more sense than 16843010 in decimal.

C++ has some support for accessing bit fields of variable width within the structures that allows to partition say 5 bit groups not easily seen in hexadecimal, or single bit groups that only binary form properly reveals. But many other languages do not have this feature and even n C++ this may be an overkill when groups are well visible in hexadecimal format as well.

$\endgroup$
0
$\begingroup$

This is not exactly an answer to the "advantages/disadvantages" question, but the way Forth does it is interesting. It supports arbitrary number bases from 2 to 36, but without needing special syntax to do so. Rather, the interpreter parses and converts numbers according to the current value of the variable BASE, which can be changed dynamically by the user's code. So the numerical value pushed by a token like 17 may be different in different parts of the code, if the value of BASE has been modified in between.

This can make things very confusing if you forget the current value of BASE, because then you have trouble resetting it. For example, at first glance you might think 10 BASE ! (the Forth equivalent of BASE = 10) would bring you back to decimal, but on further thought, you will realize that 10 BASE ! is always a no-op. You also can't easily inquire about the current base, because BASE is also used by . to format numbers for output, and so BASE @ . will likewise always print 10.

So the standard word DECIMAL is predefined, to set BASE back to ten. (And : DECIMAL 10 BASE ! ; is a valid implementation, assuming BASE is ten when compiling it, because the 10 is parsed at compile time.) HEX is also available as a standard extension, and then of course you can define more if you like.

Examples:

DECIMAL
17        ( pushes seventeen )
8 BASE !  ( set octal )
17        ( pushes fifteen ) 
DECIMAL 36 BASE !  ( set base thirty-six )
HI        ( pushes six hundred and thirty )
DECIMAL
: DUODECIMAL 12 BASE ! ;  ( define a word for base twelve )

Note that Forth has no syntax to distinguish a number from a word. Rather, each token that's encountered is looked up in the dictionary of currently defined words. If it's not found, then the interpreter attempts to parse the token as a number (using BASE). So if you had previously done : HI ." Hello world" ;, then after 36 BASE !, doing HI will print Hello world rather than pushing six hundred and thirty. (I don't know if there is a way in standard Forth to force a token in the input stream to be interpreted as a number, even if it's already defined as a word.)

This also means that you can define words for tokens that otherwise appear to be numbers. You can make use of this by doing things like 0 CONSTANT 0, which defines 0 as a word that, when executed, pushes the value 0 on the stack. The advantage is that now when the token 0 is encountered, it's handled by looking up 0 in the dictionary and executing it directly, instead of having to call the number parsing routine.

You can also use it for evil, e.g. prank your friends by doing 47 CONSTANT 2, after which 2 will push the value forty-seven, or : 1 ." It's the loneliest number" ;.

$\endgroup$
1
  • $\begingroup$ This seems like a new twist on the old (Chinese?) curse: may you live in interesting times. I've used languages that are "interesting" like that, and while they're great for short one-off stuff, they get really painful when trying to write large-scale maintainable code. I prefer to live - and code - in "less interesting" times. $\endgroup$ Nov 20, 2023 at 4:53
0
$\begingroup$

One common limitation with representing numbers in power-of-two bases is that there is no means of indicating whether all bits to the left of the leftmost represented bit should have the same value as that bit. If e.g. one knows that the bottom 8 bits of an object will all be set, and one wants to set them to 0xEA, one could write foo &= 0xFFFFFFEA if one knew that foo was 32 bits, or one could write foo &= -0x16 without having to know the length of foo, but the first form could break if the length of foo changes and the intention of the code would be to disturb the bottom 8 bits, and the latter code would make it harder to discern the intended value. Attempts to allow programmers to specify values which should be "one"-filled on the left, such as those made by some Microsoft BASIC dialects, are prone to have surprising behaviors. For example, in some Microsoft BASIC dialects, VAL("&h00010000") would yield 65536, but VAL("&h0000FFFF")` would yield -1.

$\endgroup$

You must log in to answer this question.

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