Before you begin
Why this matters
Binary and hexadecimal appear in file contents, in network data, and in library documentation. When you can read them by hand, you can check a value instead of guessing. You will also recognize a common bug: using a character’s code, fromEnum c, where you meant its digit.
What this helps you reason about
- Say the decimal value of a binary or hexadecimal number.
- Print a number in binary or hexadecimal, padded to a fixed width.
- Turn a string of digits into a number with a fold, and spot one that adds character codes.
The number eleven can be written as 11, as 1011, or as b, depending on the base. This lesson shows how a row of bits stands for a number, how Haskell writes and prints numbers in binary and hexadecimal, and how to turn a string of digits into a number yourself.
Already know this? The short version
- In base 2 each place is worth twice the place to its right, in base 16 sixteen times; 0xb and 11 are one number, and one hexadecimal digit is four bits.
- Haskell 2010 has 0x and 0o literals; 0b needs {-# LANGUAGE BinaryLiterals #-} on GHC 9.0 and is on by default from GHC 9.2. A leading 0 is not octal, so 010 is 10. Word8, from Data.Word, holds 0 to 255.
- showHex and showOct from Numeric, showIntAtBase 2 intToDigit for binary (showBin needs GHC 9.2), or printf "%08b" and "%x" from Text.Printf.
- foldl (\v c -> v * 2 + digitToInt c) 0 text reads digits from the left; fromEnum c is the character's code, not its digit.
Words in this lesson (8)
- bit
- A single binary digit: 0 or 1.
- place value
- What a digit is worth because of where it stands. In decimal, each place is worth ten times the place to its right; in binary, two times; in hexadecimal, sixteen times.
- base
- The number of different digits a way of writing numbers uses: 10 for decimal, 2 for binary, 16 for hexadecimal. Each place is worth the base times the place to its right.
- binary
- Writing numbers in base 2, with the digits 0 and 1. Binary 1011 is 8 + 2 + 1 = 11.
- hexadecimal
- Writing numbers in base 16, with the digits 0 to 9 and a to f, where a is 10 and f is 15. One hexadecimal digit stands for exactly four bits.
- literal
- A value written directly in the code, such as 11, 0xb, or 0o13.
- byte
- A unit of 8 bits, the smallest piece of memory a program usually reads or writes. One byte can hold a number from 0 to 255.
- character code
- The number a program stores for a character. The digit characters 0 to 9 have the codes 48 to 57, so the character 7 is stored as 55, not 7.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Place value
How can a row of bits stand for a number?
A bit is a single binary digit: 0 or 1. A row of bits can stand for a number, in the same way that a row of decimal digits does.
In the decimal number 345, the 3 is worth 300 because of where it stands. Each place is worth ten times the place to its right: ones, tens, hundreds. This is called place value. The number of different digits is the base, so decimal is base 10. Binary is base 2: it has the digits 0 and 1, and each place is worth two times the place to its right: 1, 2, 4, 8, and so on. So binary 1011 is 1 × 8 + 0 × 4 + 1 × 2 + 1 × 1 = 11.
You can also read the digits from left to right and keep a value so far. For each digit, multiply the value so far by 2, then add the digit. Haskell does this with a fold instead of a loop. foldl step 0 bits starts from 0 and applies step to the value so far and each bit in turn, from left to right. Nothing is reassigned: each step gives a new value.
fromBits :: [Int] -> Int
fromBits bits = foldl step 0 bits
where
step value bit = value * 2 + bit
-- fromBits [1, 0, 1, 1] is 11 Each digit is worth the digit times its place. Reading from the left, multiply the value so far by the base and add the next digit.
Precisely: why multiplying works
After the last digit, the first digit has been multiplied by the base once for every digit after it, which is exactly its place value. With 10 instead of 2, the same steps read decimal digits: 3, then 34, then 345. The experiment uses scanl, which works like foldl but keeps every value so far in a list, so print can show each step.
Experiment
Build a number one bit at a time
Multiplying by 2 moves every bit already read one place to the left, which doubles what it is worth. Adding the next bit fills the new ones place. After four bits, the first 1 has been doubled three times, so it is worth 8.
Why it matters The same digits mean different numbers in different bases. The base is the number you multiply by.
Predict the printed list. Then change value * 2 to value * 10 and predict again.
main :: IO ()
main = do
let bits = [1, 0, 1, 1] :: [Int]
step value bit = value * 2 + bit
print (scanl step 0 bits)
What to look for
GHCi shows one location per stop, not a stack of frames. At the print line, bits and step show _ (unevaluated). print writes [0 before any step is computed. Then GHCi stops in step once per bit, showing value and bit: value 0 with bit 1, then value 1 with bit 0, then 2, then 5. Each new number appears in the output as soon as print needs it, and the run prints [0,1,2,5,11]. After the edit, the run prints [0,1,10,101,1011]: the same digits read as a decimal number.
Expected output
[0,1,2,5,11]02 / Literals
How do you write a number in binary or hexadecimal?
A value written directly in the code is a literal. 0xb is a hexadecimal literal: it is the number 11, and so is the decimal literal 11. 0o13 is an octal literal, base 8, and is 11 too. A binary literal, 0b1011, is not part of Haskell 2010. On GHC 9.0, it needs the BinaryLiterals extension, switched on by a line at the top of the file. Without that line, GHC reads 0b1011 as 0 followed by a name, and reports error: Variable not in scope: b1011. The base changes how a number is written, not the number.
Hexadecimal is base 16. It needs sixteen digits, so after 0 to 9 it uses the letters a to f: a is 10, b is 11, and f is 15. Capital letters A to F mean the same. Each place is worth sixteen times the place to its right, so hexadecimal 2a is 2 × 16 + 10 = 42.
Programmers use hexadecimal because one hexadecimal digit stands for exactly four bits: four bits have 16 patterns. Hexadecimal b is binary 1011, and hexadecimal 2a is binary 0010 1010: the 2 gives the left four bits and the a gives the right four.
{-# LANGUAGE BinaryLiterals #-}
main :: IO ()
main = do
print (0b1011 == (0xb :: Int)) -- True
print (0x2a :: Int) -- 42 A binary or hexadecimal literal is another way to write the same number. One hexadecimal digit is four bits.
Precisely: GHC2021 and separators
From GHC 9.2, the default language GHC2021 includes BinaryLiterals, so 0b1011 works without the line. The NumericUnderscores extension allows 0b1011_0110 for reading. A leading 0 does not mean octal in Haskell: 010 is ten.
03 / Bytes
What is a byte, and what can it hold?
A byte is a group of 8 bits. Memory and files are measured in bytes.
Each bit has two possible values, so 8 bits have 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 = 256 different patterns. Read as a number, they run from 00000000, which is 0, to 11111111, which is 255. In hexadecimal, a byte is always two digits, from 00 to ff. That is why tools often show bytes as pairs of hexadecimal digits, such as 48 69 ff.
In Haskell, Word8 from Data.Word holds exactly the numbers 0 to 255. Int is wider and is the usual type for counting. What happens to a value that does not fit, and how negative numbers are stored, is the subject of a later lesson, Integers, signs, and overflow.
import Data.Word (Word8)
top :: Word8
top = 0xff
-- top is 255, and maxBound :: Word8 is 255 A byte is 8 bits: 256 patterns, read as 0 to 255, or two hexadecimal digits.
04 / Printing
How do you print a number in binary or hexadecimal?
print shows a number in decimal. The Numeric module turns a number into digits of another base: showHex 42 "" is "2a", and showOct 15 "" is "17". The "" is the text to put after the digits. For binary, showIntAtBase 2 intToDigit 42 "" gives "101010", with intToDigit from Data.Char.
printf, from Text.Printf, takes a format string, in which %b prints binary digits, %x hexadecimal, and %d decimal. A number after % is a width, the smallest number of characters to print. A 0 in front of it asks for zeros instead of spaces, so %08b prints all 8 bits of a byte. The width never removes digits.
import Text.Printf (printf)
main :: IO ()
main = do
let value = 42 :: Int
printf "%b\n" value -- 101010
printf "%08b\n" value -- 00101010
printf "%x\n" value -- 2a showHex and printf "%x" give hexadecimal digits; printf "%08b" pads binary to a width and never cuts.
Precisely: showBin
Numeric.showBin was added in base 4.16, which comes with GHC 9.2. On the runner’s GHC 9.0, import Numeric (showBin) gives error: Module ‘Numeric’ does not export ‘showBin’.
Experiment
Print one number in binary and hexadecimal
printf from Text.Printf has %b for binary digits and %x for hexadecimal digits, with no prefix. In %08b, 0 asks for zeros and 8 is the width. The width 8 is a minimum: zeros are added on the left until there are 8 digits, and nothing is ever removed. 300 is 256 + 32 + 8 + 4, so it needs a ninth place, worth 256.
Why it matters Padding to 8 digits shows every bit of a byte, but it does not limit a number to one byte. A number above 255 prints with more digits.
Predict the three printed lines. Then change 11 to 300 and predict again: how many binary digits does 300 need?
import Text.Printf (printf)
main :: IO ()
main = do
let value = 11 :: Int
printf "%b\n" value
printf "%08b\n" value
printf "%x\n" value
What to look for
GHCi stops at the first printf line with value unevaluated, then stops in value when printf needs it. The output then grows one line per stop: 1011, then 00001011, then b. After the edit, the run prints 100101100 twice, because %08b adds no zeros to 9 digits, then 12c.
Expected output
1011
00001011
b05 / Digits in text
How do you turn a string of digits into a number?
Text such as "101" is not a number. It is a string of three characters. A program stores each character as a number of its own, called its character code. The digit characters 0 to 9 have the codes 48 to 57, in order. So the character 1 is stored as 49, not as 1, and the character 7 as 55.
To get the value of a digit character, subtract the code of the character 0: 49 - 48 is 1. Then use the loop from the first section. In Haskell, a String is a list of Char values. fromEnum c gives the code of the character c, so fromEnum c - fromEnum '0' is the digit's value. digitToInt, from Data.Char, gives the same value directly: digitToInt '7' is 7. The fold reads the characters from the left, like the fold in the first section.
import Data.Char (digitToInt)
fromBinary :: String -> Int
fromBinary text = foldl step 0 text
where
step value digit = value * 2 + digitToInt digit
-- fromBinary "101" is 5 Turn each digit character into its digit, then multiply the value so far by the base and add.
Coming from Python: ord('7') corresponds to fromEnum '7', 55; the digit is digitToInt '7', 7.
Precisely: readers in Numeric
Numeric.readHex and readOct read digits from the start of a string and return the number with the rest of the string, as in readHex "2ag" == [(42, "g")]: a character that is not a digit ends the number without an error. digitToInt stops the program with an error for a character that is not a hexadecimal digit.
Experiment
Add the digits of a binary string
fromEnum digit gives the character's code, not its digit. The character 1 is stored as its code, 49, and the character 0 as 48. Adding the code instead of the digit gives 49, then 49 × 2 + 48 = 146, then 146 × 2 + 49 = 341. Subtracting the code of 0 turns each code into its digit: 49 - 48 is 1.
Why it matters A digit character is not its digit. Subtract the code of the character 0 before you multiply and add.
The fold is meant to end at 5 for "101". Predict the printed list. Then change fromEnum digit to (fromEnum digit - fromEnum '0') and predict again.
main :: IO ()
main = do
let text = "101"
step value digit = value * 2 + fromEnum digit
print (scanl step 0 text)
What to look for
As print needs each new value, GHCi stops in step, where digit shows '1' or '0' and, at most stops, value shows the value so far. The list grows in the output to [0,49,146,341]. After the edit, the run prints [0,1,2,5].
Expected output
[0,49,146,341]06 / Try it
Can you explain a fresh example?
Work out what each literal is worth, then how each number is printed. The next lesson, Function calls, follows the values that functions receive and return.
import Text.Printf (printf)
main :: IO ()
main = do
let a = 0x1f :: Int
b = 0o10 :: Int
digit = '7'
printf "%d %x %d\n" (a + b) (a + b) (fromEnum digit)
What is printed?
Reveal the reasoning
39 27 55. 0x1f is 1 × 16 + 15 = 31, and 0o10 is octal, so it is 8; a + b is 39. In hexadecimal, 39 is 2 × 16 + 7, which prints as 27: the same digits as decimal twenty-seven, but a different number. fromEnum digit is the code of the character 7, which is 55.
Practice tickets
- BITS-101 · The pill counter shows 3 for a tray of 6 pills → Free
- BITS-102 · Heart rates saved with a letter come out wrong → Sign in · Pro
- BITS-103 · Impossible hearing aid settings are saved as other values → Sign in · Pro