Scala - Numeric Types



Scala numeric types provide basic understanding and managing numbers. It ensures accuracy in applications and improves coding. In this tutorial, we will understand about numeric types in Scala.

Basic of Scala Numeric Types

Scala provides various numeric types for computational needs. Developers can handle various formats and can ensure efficiency and precision in calculations. These numeric types are Integral types, Floating types. We will also discuss their Type ranges. We have discussed these in brief below.

1. Integral Types

In Scala, there are various types of Integral types to represent whole numbers. These integral types are Short, Int, Long and Byte. Each type has a specific range. For example,

val byteVal: Byte = 125
val shortVal: Short = 32767
val intVal: Int = 2135482547
val longVal: Long = 9223372036854775807L

2. Floating-Point Types

Scala has Float and Double for decimal numbers. You can choose based on precision needs. For example,

val floatVal: Float = 2.718F
val doubleVal: Double = 2.718281828459045

3. Type Ranges

Integral types and Floating types have their specific ranges. So you can choose the right type for specific tasks. Here is brief of their ranges:

Type Min Value Max Value
Byte -128 127
Short -32768 32767
Int -2147483648 2147483647
Long -9223372036854775808 9223372036854775807

Numeric Types: Integers and Floating Points

In Scala, there are two types of numbers: Integers and Floating Points. These are basic types of numbers used for various mathematical calculations in Scala programs. These are important for simple arithmetic and complex algorithms.

1. Arithmetic Operations

You can perform arithmetic operations with Integers in a straightforward way. You need to use operators like, *,/,+,- etc. For example,

val difference: Int = 8 - 3
val quotient: Int = 15 / 3
val sum: Int = 5 + 3
val product: Int = 5 * 3
val power: Int = 2 ^ 3

For Floating Point numbers the result can be different but operators are always the same. For example,

val difference: Float = 8.0f - 3.0f
val quotient: Float = 15.0f / 3.0f
val sum: Float = 5.0f + 3.0f
val product: Float = 5.0f * 3.0f
val power: Float = math.pow(2.0, 3.0).toFloat

2. Type Casting

You may require conversion from Integers to Floating Point numbers and vice-versa. So you can use Type Casting for these conversions. In Scala, methods like toFloat and toInt are used here. For example,

val integerValue: Int = 10
val doubleValue: Double = integerValue.toDouble

And conversely,

val floatValue: Float = 7.5f
val integerValue: Int = floatValue.toInt

Numeric Types - Comparisons and Equality

Sometimes, you need to compare integers and floating points. You can do this using standard comparison symbols like ==, !=, <, >, <=, and >=. For example,

val isNotEqual: Boolean = 8 != 3
val isLessOrEqual: Boolean = 2.0 <= 2.5

While comparing floating points, you need to be careful about precision problems. Tiny rounding errors can cause surprising outcomes. It is better to see if two floating points are 'close enough' instead of precisely equal.

Precision and Limitations

Precision and inherent limitations of numeric types are important. Scala has defined boundaries and behaviors for its numeric types that developers need to know about.

1. Floating-Point Precision

In Scala, Floating Point numbers are represented by Float and Double. These two have inherent and precision limitations. You can store decimal values but they might not always be stored accurately. For example,

val sumResult: Double = 1.5 + 2.7
println(sumResult)  // Might not be exactly 4.2 due to precision issues

2. Integer Overflow

In Scala, Integers are fixed size. Because of fixed size, these can have underflow and overflow scenarios. When an integer exceeds its minimum value then it wraps to maximum value and vice-versa. For example,

val minInt: Int = Int.MinValue
val underflowed: Int = minInt - 1

3. Rounding Errors

Dealing with floating-point numbers can be tricky due to rounding errors. These errors occur because binary systems may not always perfectly represent certain decimal fractions. For example,

val roundedValue: Double = Math.round(0.785 * 100) / 100.0
println(roundedValue)  // Might not be exactly 0.79 due to rounding errors

4. Safe Operations

To avoid problems with precision, especially when working with floating points, you can use libraries for arbitrary precision arithmetic. These tools can give you accurate results.

Type Conversion and Casting Between Numeric Types

In Scala, you may need data conversion, i.e., convert one data type into another type. This process is known as Casting. It is used to reduce error while representing floating-point format.

1. Implicit and Explicit Conversion

Scala has both Explicit and Implicit type conversion. Implicit occurs automatically when context requires it. Whereas, developers can do explicit conversion when required. For example,

val floatValue: Float = 7.5f
val intValue: Int = floatValue.toInt  // Implicit conversion

And explicit conversion:

val floatVal2: Float = 3.14f
val intVal2: Int = floatVal2.toInt  // Explicit conversion

There is a chance for losing data when you cast from larger one to smaller one. For example,

val largeFloat: Float = 1.5e7f
val castedInt: Int = largeFloat.toInt  // Potential data loss

You should be aware about it before conversion and handle such errors. In Scala, there is a hierarchy for Numeric types. This hierarchy is given as follows below.

Type Can widen to
Byte Short, Int, Long, Float, Double
Short Int, Long, Float, Double
Int Long, Float, Double
Long Float, Double

2. Generic Numeric Operations

In Scala, Numeric types provide versatility and precision. You can create generic functions that operate on any numeric types. For example, if you require a function to work with any numeric type. Like this Haskell approach:

case class NumericPair[A](first: A, second: A)(implicit num: Numeric[A])

You can perform various operations like subtraction, addition, multiplication and division using Numeric[A] traits. It is not extended by basic types like Float and Int. It provides implicit objects. For example, num.plus(a, b) instead of a+b.

3. Custom Numeric Types

Sometimes, inbuilt numeric types don't meet your needs. For example, if you want to define a custom Decimal type for accurate calculations. Though you can not extend the Numeric trait directly. But you can create an instance of this type.

final class CustomNumber(val value: Int) extends AnyVal

object CustomNumber {
   implicit val numeric: Numeric[CustomNumber] = new Numeric[CustomNumber] {
      // Define operations like plus, minus, times, etc.
   }
}

4. Vector operations with Numeric Types

Vector is a data structure used in many applications, like physics and graphics simulations. While creating a generic Vector class in Scala, you may require it compatible with all numeric types.

class Point2D[@specialized T](val x: T, val y: T)(implicit num: Numeric[T]) {
   def +(other: Point2D[T]) = new Point2D(num.plus(x, other.x), num.plus(y, other.y))
}

This approach involves using the `@specialized` annotation to optimize for specific types. It ensures efficient operations without the overhead of boxing.

5. Safe Division and Handling

You can handle division by zero using the Option type. For example,

def safeDivision(dividend: Int, divisor: Int): Option[Int] = if (divisor == 0) None else Some(dividend / divisor)

You need to use `map` function for the result:

safeDivision(6, 2).map(_ * 6)  // Returns Option[Int] with value Some(18)

Optimizing Performance with Numeric Types

You can optimize Scala programs with the help of Numeric types.

Specialized Annotations

When specialized Annotations are used in the Scala program, then the compiler generates optimized bytecode and reduces overhead unboxing and boxing operations. For example,

def multiply[@specialized(Int, Double) T](a: T, b: T)(implicit numeric: Numeric[T]): T = numeric.times(a, b)

Boxing and Unboxing are processes which have wrapping (boxing) primitive types into their object counterparts and vice versa. It can have performance overhead. You can use value classes to avoid this overhead. For example,

final class EnhancedDouble(val value: Double) extends AnyVal {
   def cube: Double = value * value * value
}

There will not be any object overhead because of AnyVal extension.

Note that there are BigInt and BigDecimal in scala for large number operations. But these are immutable and can be slower than primitive types.

Advertisements