Interfaces, Structs and Classes
As in the case with other low-level, Julia and Golang store collection of fields in a data structure called structs.
Dart, Python and JavaScript use classes. A class is a template definition of the methods and variables in a particular kind of object.
Let's define a collection of fields, Vertex, for two values, X and Y, both integers while observing the respective data structure:
package main
import "fmt"
type Vertex struct {
X int
Y int
// if fields are of same type
// they could be written as
// X, Y int
}
func main() {
v := Vertex{1, 2}
fmt.Println(v)
}
class Vertex {
X: number
Y: number
constructor(X: number, Y: number){
this.X = X
this.Y = Y
}
toString(){
return `{${this.X} ${this.Y}}`;
}
}
let v: Vertex = new Vertex(1, 3)
console.log(v) // Vertex { X: 1, Y: 3 }
console.log(`${v}`) // using string literals in log prints toString, {1 3}
class Vertex {
int x;
int y;
Vertex(this.x, this.y);
@override
String toString() => "{$x $y}";
}
void main() {
var v = Vertex(1, 3);
print(v); // {1 3}
}
class Vertex:
def __init__(self, X: int, Y: int) -> None:
self.X = X
self.Y = Y
def __str__(self) -> str:
return "{%d %d}" %(self.X, self.Y)
v = Vertex(1, 3)
print(v)
struct Vertex
X::Int
Y::Int
end
Base.show(io::IO, vert::Vertex) = print(io, "{$(vert.X) $(vert.Y)}")
v = Vertex(1, 3)
println(v)
Struct fields are accessed using a dot (the dot notation)
v_x = v.X
# To update a value, use the syntax below
v.X = 34
In Julia, structs are immutable. To make them mutable, you have to add mutable to the struct
mutable struct Vertex
X::Int
Y::Int
end
Let's now see how to initialize classes with default values:
v1 = Vertex{1, 2} // all fields initialized
v2 = Vertex{X: 1} // Y:0 is implicit
v3 = Vertex{} // X:0 and Y:0
class Vertex {
X: number
Y: number
constructor({X = 0, Y = 0} = {}){
this.X = X
this.Y = Y
}
}
// let v: Vertex = new Vertex({ Y: 78 })
class Vertex {
int x;
int y;
Vertex({this.x = -1, this.y = 0});
}
//var v = Vertex(x:7, y:15);
class Vertex:
def __init__(self, X: int = 0, Y: int = 0) -> None:
self.X = X
self.Y = Y
# v = Vertex(Y=5)
Methods
A method in object-oriented programming (OOP) is a procedure associated with a message and an object. An object consists of state data and behavior; these compose an interface, which specifies how the object may be utilized by any of its various consumers. A method is a behavior of an object parametrized by a consumer.
Go does not have classes. However, you can define methods on types. A method is a function with a special receiver argument. The receiver appears in its own argument list between the func keyword and the method name.
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}
class Vertex {
X: number
Y: number
constructor({X = 0, Y = 0} = {}){
this.X = X
this.Y = Y
}
Abs(): number {
return Math.sqrt(this.X ** 2 + this.Y ** 2)
}
toString(){
return `{${this.X} ${this.Y}}`;
}
}
let v: Vertex = new Vertex({ X: 8, Y: 15})
console.log(v.Abs())
import 'dart:math';
class Vertex {
// final does not allow modification
// of value after initialization
final int x;
final int y;
Vertex({this.x = 0, this.y = 0});
double Abs() {
return sqrt(pow(x, 2) + pow(y, 2));
}
@override
String toString() => "{$x $y}";
}
void main() {
var v = Vertex(x:8, y:15);
print(v.Abs());
}
from math import sqrt, pow
class Vertex:
def __init__(self, X: int = 0, Y: int = 0) -> None:
self.X = X
self.Y = Y
def __str__(self) -> str:
return "{%d %d}" %(self.X, self.Y)
def Abs(self) -> float:
return sqrt(pow(self.X, 2) + pow(self.Y, 2))
v = Vertex(X=8, Y=15)
print(v.Abs())
mutable struct Vertex
X::Int
Y::Int
end
function Abs(vert::Vertex)::Float64
return sqrt(vert.X ^ 2 + vert.Y ^ 2)
end
Base.show(io::IO, vert::Vertex) = print(io, "{$(vert.X) $(vert.Y)}")
v = Vertex(8, 15)
println(Abs(v))
Structs in Julia without mutable is like declaring variables in Dart class with final
Methods continued
In Golang, you can declare a method on non-struct types, too. In this example we see a numeric type MyFloat with an Abs method.
You can only declare a method with a receiver whose type is defined in the same package as the method. You cannot declare a method with a receiver whose type is defined in another package (which includes the built-in types such as int).
package main
import (
"fmt"
"math"
)
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}
Pointer Receivers
In Go, methods with pointer receivers can modify the value to which the receiver points (as Scale does here below). Since methods often need to modify their receiver, pointer receivers are more common than value receivers.
In other languages, passing an object/struct as a parameter to a function actually 'passes it as reference', so modifying the fields will change the value in the passed struct/object
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
// Value Receiver
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
// Pointer Receiver
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
v.Scale(5)
fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}
class Vertex {
X: number
Y: number
constructor({X = 0, Y = 0} = {}){
this.X = X
this.Y = Y
}
Abs(): number {
return Math.sqrt(this.X ** 2 + this.Y ** 2)
}
Scale(f: number): void {
this.X = this.X * f
this.Y = this.Y * f
}
toString(){
return `{${this.X} ${this.Y}}`;
}
}
let v: Vertex = new Vertex({ X: 3, Y: 4})
console.log(`Before scaling: ${v}, Abs: ${v.Abs()}`)
v.Scale(5);
console.log(`After scaling: ${v}, Abs: ${v.Abs()}`)
import 'dart:math';
class Vertex {
double x;
double y;
Vertex({this.x = 0, this.y = 0});
double Abs() {
return sqrt(pow(x, 2) + pow(y, 2));
}
void Scale(double f) {
x = x * f;
y = y * f;
}
@override
String toString() => "{$x $y}";
}
void main() {
var v = Vertex(x:3, y:4);
print("Before scaling: $v, Abs: ${v.Abs()}");
v.Scale(5);
print("After scaling: $v, Abs: ${v.Abs()}");
}
from math import sqrt, pow
class Vertex:
def __init__(self, X: int = 0, Y: int = 0) -> None:
self.X = X
self.Y = Y
def __str__(self) -> str:
return "{%d %d}" %(self.X, self.Y)
def Abs(self) -> float:
return sqrt(pow(self.X, 2) + pow(self.Y, 2))
def Scale(self, f: float) -> None:
self.X = self.X * f
self.Y = self.Y * f
v = Vertex(X = 3, Y = 4)
print(f"Before scaling: {v}, Abs: {v.Abs()}")
v.Scale(5)
print(f"After scaling: {v}, Abs: {v.Abs()}")
module Fourier
mutable struct Vertex
X::Int
Y::Int
end
function Abs(vert::Vertex)::Float64
return sqrt(vert.X ^ 2 + vert.Y ^ 2)
end
function Scale(vert::Vertex, f::Float64)
vert.X = vert.X * f
vert.Y = vert.Y * f
end
Base.show(io::IO, vert::Vertex) = print(io, "{$(vert.X) $(vert.Y)}")
v = Vertex(3, 4)
println("Before scaling: $v, Abs: $(Abs(v))")
Scale(v, convert(Float64, 5))
println("After scaling: $v, Abs: $(Abs(v))")
end
siliconSavanna@Fourier-PC [Golang]$ go run test.go
Before scaling: {X:3 Y:4}, Abs: 5
After scaling: {X:15 Y:20}, Abs: 25
[Golang]$
Rewriting the methods Abs and Scale as functions in Go, they will be as below:
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func Scale(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
Methods and Pointer indirection
In Golang, functions with a pointer argument must take a pointer, while methods with pointer receivers take either a value or a pointer as the receiver when they are called.
The equivalent thing happens in the reverse direction. Functions that take a value argument must take a value of that specific type:
var v Vertex
fmt.Println(AbsFunc(v)) // OK
fmt.Println(AbsFunc(&v)) // Compile error!
The codebase below shows a method taking both a value and a pointer and working ok.
var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK
// In this case, the method call p.Abs()
// is interpreted as (*p).Abs().
There are two reasons to use a pointer receiver.
~ The first is so that the method can modify the value that its receiver points to.
~ The second is to avoid copying the value on each method call. This can be more efficient if the receiver is a large struct, for example.
In general, all methods on a given type should have either value or pointer receivers, but not a mixture of both.
Note: If you want to print struct with its field names, use %+v in fmt.Prinf
Interfaces
An interface or protocol type is a data type describing a set of method signatures, the implementations of which may be provided by multiple classes that are otherwise not necessarily related to each other. A class which provides the methods listed in a protocol is said to adopt the protocol, or to implement the interface.
Interfaces in Julia are very much different from the other languages.
package main
import (
"fmt"
"math"
)
type Abser interface {
Abs() float64
}
type MyFloat float64
type Vertex struct {
X, Y float64
}
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f // a MyFloat implements Abser
fmt.Println(a.Abs())
a = &v // a *Vertex implements Abser
fmt.Println(a.Abs())
/* In the following lines, v is a Vertex (not *Vertex)
and does NOT implement Abser.
a = v
fmt.Println(a.Abs()) // Error
*/
}
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
interface Abser {
Abs(): number
}
/* To use Abser interface for object literals
you have to extend Abser interface */
interface MyFloat extends Abser {
value: number
}
/* OR
type MyFloat = Abser & {
value: number
} */
class Vertex implements Abser {
X: number
Y: number
constructor({X = 0, Y = 0} = {}){
this.X = X
this.Y = Y
}
Abs(): number {
return Math.sqrt(this.X ** 2 + this.Y ** 2)
}
toString(){
return `{${this.X} ${this.Y}}`;
}
}
var f: MyFloat = {
value : -Math.sqrt(2),
Abs(): number {
if (this.value < 0) {
return -this.value
}
return this.value
}
};
let v: Vertex = new Vertex({ X: 3, Y: 4})
console.log(v.Abs())
console.log(f.Abs())
import 'dart:math';
class Abser {
double Abs() => 0;
}
class Vertex implements Abser {
final double x;
final double y;
Vertex({this.x = 0, this.y = 0});
@override
double Abs() {
return sqrt(pow(x, 2) + pow(y, 2));
}
@override
String toString() => "{$x $y}";
}
void main() {
var v = Vertex(x:3, y:4);
print(v.Abs());
}
from abc import ABC, abstractmethod
from math import sqrt, pow
class Abser(ABC):
@abstractmethod
def Abs(self) -> float:
pass
class Vertex(Abser):
def __init__(self, X: int = 0, Y: int = 0) -> None:
self.X = X
self.Y = Y
def __str__(self) -> str:
return "{%d %d}" %(self.X, self.Y)
def Abs(self) -> float:
return sqrt(pow(self.X, 2) + pow(self.Y, 2))
v = Vertex(X = 3, Y = 4)
print(v.Abs())
module Fourier
#= We'll talk about installing third-party packages
in the lesson Custom Packages. You can skip to it
first if you like
=#
# You must call the line below within module, else functions won't be accessible
using Statistics
struct Squares
count::Int
end
#= The loop below won't work until
we define a method iterate from the
Base.iterate for the struct Square =#
Base.iterate(S::Squares, state=1) = state > S.count ? nothing : (state*state, state+1)
for i in Squares(7)
println(i)
end
println(25 in Squares(10))
println(mean(Squares(100))) # mean from Statistics
#= for collect to work, we need to define
two methods for the struct Squares =#
Base.length(S::Squares) = S.count
#=
Defining the method above, we are to able to
call collect. However the vector formed will
have type Any. We know the values of Iterable
will always be int so we can constrain the type
to int with the line below =#
Base.eltype(::Type{Squares}) = Int
#=The result will be vector of type int=#
println(collect(Squares(4)))
# This would be it with interfaces for now
end
The empty interface
In Golang, the interface type that specifies zero methods is known as the empty interface. An empty interface may hold values of any type. (Every type implements at least zero methods.) Empty interfaces are used by code that handles values of unknown type. For example, fmt.Print takes any number of arguments of type interface{}
package main
import "fmt"
func main() {
var i interface{}
describe(i)
i = 42
describe(i)
i = "hello"
describe(i)
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
[Golang]$ go run test.go
(<nil>, <nil>)
(42, int)
(hello, string)
[Golang]$
Type Assertions
A type assertion provides access to an interface value's underlying concrete value.
This statement asserts that the interface value i holds the concrete type T and assigns the underlying T value to the variable t.
If i does not hold a T, the statement will trigger a panic.
To test whether an interface value holds a specific type, a type assertion can return two values: the underlying value and a boolean value that reports whether the assertion succeeded.
If i holds a T, then t will be the underlying value and ok will be true.
If not, ok will be false and t will be the zero value of type T, and no panic occurs.
package main
import "fmt"
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
f = i.(float64) // panic
fmt.Println(f)
}
[Golang]$ go run test.go
hello
hello true
0 false
panic: interface conversion: interface {} is string, not float64
goroutine 1 [running]:
main.main()
/Users/siliconSavanna/Projects/Golang/test.go:17 +0x14c
exit status 2
[Golang]$
Type switches
A type switch is a construct that permits several type assertions in series.
A type switch is like a regular switch statement, but the cases in a type switch specify types (not values), and those values are compared against the type of the value held by the given interface value
package main
import "fmt"
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
[Golang]$ go run test.go
Twice 21 is 42
"hello" is 5 bytes long
I don't know about type bool!
[Golang]$
Stringers
One of the most ubiquitous interfaces is Stringer defined by the fmt package.
type Stringer interface {
String() string
}
A Stringer is a type that can describe itself as a string. The fmt package (and many others) look for this interface to print values.
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a)
fmt.Println(z)
}
[Golang]$ go run test.go
Arthur Dent (42 years)
Zaphod Beeblebrox (9001 years)
[Golang]$
Inheritance
Inheritance enables you to define a class that takes all the functionality from a parent class and allows you to add more. Using class inheritance, a class can inherit all the methods and properties of another class. Inheritance is a useful feature that allows code reusability.
In the codebase below, we have a (base) class Television. It is just a normal class so to say. Now, we'd like to borrow all it's attributes (variables) and methods and use them in another class, SmartTelevision. We don't want to rewrite all this since it would be verbose code and unconventional hence just inherit them. Hence we extend the class Television to SmartTelevision
Now, we have access to turnOn method and attribute brand in Base class Television from our new class SmartTelevision
The super in the constructor is used to access the Base attributes
class Television {
brand: string
constructor(brand: string) {
this.brand = brand;
}
turnOn() {
console.log(`TV Brand: ${this.brand}`);
}
}
class SmartTelevision extends Television{
memory: string
constructor(brand: string, memory: string) {
super(brand)
this.memory = memory
}
}
let smartTV = new SmartTelevision("LG Nanocell", "128GB")
smartTV.turnOn()
void main(){
var smartTV = SmartTelevision("LG NanoCell", "128GB");
smartTV.turnOn();
}
class Television {
final String brand;
Television(this.brand);
/* works with named arguments too
* Television({required this.brand});
*/
void turnOn() {
print("TV Brand: ${brand}");
}
}
class SmartTelevision extends Television{
final String memory;
/* Before Dart 2.17
* SmartTelevision(String brand, this.memory) : super(brand);
*/
// New way
SmartTelevision(super.brand, this.memory);
/* For named arguments
* SmartTelevision({required super.brand, required this.memory});
*/
}
class Television:
def __init__(self, brand):
self.brand = brand
def turnOn(self):
print(f"TV Brand: {self.brand}")
class SmartTelevision(Television):
def __init__(self, brand, memory):
super().__init__(brand)
self.memory = memory
smartTV = SmartTelevision("LG NanoCell", "128GB")
smartTV.turnOn()
module Fourier
abstract type BaseTV end
mutable struct Television <: BaseTV
brand::String
year::Int64
end
mutable struct SmartTelevision <: BaseTV
tv::Television
memory::String
end
function turnOn(genericTV::BaseTV)
brand = isa(genericTV, Television) ? genericTV.brand : genericTV.tv.brand
println("TV Brand: $brand")
end
tv = Television("LG Nanocell", 2022)
smartTV = SmartTelevision(Television("LG Smart Nanocell", 2022), "128GB")
turnOn(tv)
turnOn(smartTV)
end