UNPKG

6 kBPlain TextView Raw
1/*!
2 * Copyright (c) 2017-2018 by The Funfix Project Developers.
3 * Some rights reserved.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18import { Setoid } from "funland"
19
20/**
21 * Interface for testing the equality of value objects.
22 */
23export interface IEquals<A> {
24 /**
25 * Indicates whether some other object is "equal to" this one.
26 *
27 * Properties:
28 *
29 * - reflexive: for any value, `x.equals(x) == true`
30 * - symmetric: for any values x and y, `x.equals(y) == y.equals(x)`
31 * - transitive: `x.equals(y) && y.equals(z) => x.equals(z)`
32 * - consistent: `x.equals(y)` always yields the same result
33 *
34 * Rule: equal objects MUST have equal hash codes!
35 */
36 equals(other: A): boolean
37
38 /**
39 * Returns a hash code value for this value.
40 *
41 * This method is supported for the benefit of hash tables.
42 *
43 * Properties:
44 *
45 * - consistent: multiple invocations always yield the same result
46 * - if `x.equals(y) == true` then `x.hashCode() == y.hashCode()`
47 * - if `x.equals(y) == false` it is NOT required for their hash codes
48 * to be equal, i.e. this function is not injective
49 */
50 hashCode(): number
51}
52
53/**
54 * Test if the given reference is a value object.
55 *
56 * Value objects are objects that implement the [[IEquals]]
57 * interface.
58 *
59 * @param ref is the reference to test
60 */
61export function isValueObject(ref: any): boolean {
62 return !!(ref &&
63 typeof ref.equals === "function" &&
64 typeof ref.hashCode === "function")
65}
66
67/**
68 * Tests for universal equality.
69 *
70 * First attempting a reference check with `===`,
71 * after which it tries to fallback on [[IEquals]], if the
72 * left-hand side is implementing it.
73 *
74 * ```typescript
75 * equals(10, 10) // true, because 10 === 10
76 *
77 * class Box implements IEquals<Box> {
78 * constructor(value: number) { this.value = value }
79 *
80 * equals(other) { return this.value === other.value }
81 * hashCode() { return this.value << 2 }
82 * }
83 *
84 * // false, because they are not the same reference
85 * new Box(10) === new Box(10)
86 *
87 * // true, because `Box#equals` gets called
88 * equals(new Box(10), new Box(10))
89 * ```
90 */
91export function is<A>(lh: A, rh: A): boolean {
92 if (lh === rh || (lh !== lh && rh !== rh)) {
93 return true
94 }
95 if (!lh || !rh) {
96 return false
97 }
98 /* istanbul ignore else */
99 /* tslint:disable-next-line:strict-type-predicates */
100 if (typeof lh.valueOf === "function" && typeof rh.valueOf === "function") {
101 const lh2 = lh.valueOf()
102 const rh2 = rh.valueOf()
103 if (lh2 === rh2 || (lh2 !== lh2 && rh2 !== rh2)) {
104 return true
105 }
106 if (!lh2 || !rh2) {
107 return false
108 }
109 }
110 // noinspection PointlessBooleanExpressionJS
111 return !!(
112 isValueObject(lh) &&
113 (lh as any).equals(rh)
114 )
115}
116
117/** Alias for [[is]]. */
118export function equals<A>(lh: A, rh: A): boolean {
119 return is(lh, rh)
120}
121
122/**
123 * Returns a `Setoid` type-class instance that depends
124 * universal equality, as defined by {@link is}.
125 */
126export const universalSetoid: Setoid<any> = { equals }
127
128/**
129 * Universal hash-code function.
130 *
131 * Depending on the given value, it calculates the hash-code like so:
132 *
133 * 1. if it's a `number`, then it gets truncated
134 * to an integer and returned
135 * 2. if it's a "value object" (see [[isValueObject]]), then
136 * its `hashCode` is used
137 * 3. if a `valueOf()` function is provided, then the
138 * `hashCode` gets recursively invoked on its result
139 * 4. if all else fails, the value gets coerced to a `String`
140 * and a hash code is calculated using [[hashCodeOfString]]
141 *
142 * @param ref is the value to use for calculating a hash code
143 * @return an integer with the aforementioned properties
144 */
145export function hashCode(ref: any): number {
146 if (typeof ref === "number") {
147 return ref & ref
148 }
149 /* istanbul ignore else */
150 if (typeof ref.valueOf === "function") {
151 const v = ref.valueOf()
152 if (v !== ref) return hashCode(v)
153 }
154 if (isValueObject(ref)) {
155 return (ref as IEquals<any>).hashCode()
156 }
157 return hashCodeOfString(String(ref))
158}
159
160/**
161 * Calculates a hash code out of any string.
162 */
163export function hashCodeOfString(str: string): number {
164 let hash = 0
165 /* tslint:disable-next-line:strict-type-predicates */
166 if (str == null || str.length === 0) return hash
167 for (let i = 0; i < str.length; i++) {
168 const character = str.charCodeAt(i)
169 hash = ((hash << 5) - hash) + character
170 hash = hash & hash // Convert to 32bit integer
171 }
172 return hash
173}
174
175/** The identity function. */
176export function id<A>(a: A): A {
177 return a
178}
179
180/**
181 * Utility function for implementing mixins, based on the
182 * [TypeScript Mixins]{@link https://www.typescriptlang.org/docs/handbook/mixins.html}
183 * documentation.
184 *
185 * Sample:
186 *
187 * ```typescript
188 * class Disposable { ... }
189 * class Activatable { ... }
190 * class SmartObject implements Disposable, Activatable { ... }
191 *
192 * applyMixins(SmartObject, [Disposable, Activatable]);
193 * ```
194 *
195 * Using `implements` instead of `extends` for base classes
196 * will make the type system treat them like interfaces instead of
197 * classes. And by `applyMixins` we can also supply global
198 * implementations for the non-abstract members.
199 */
200export function applyMixins(derivedCtor: {prototype: any}, baseCtors: {prototype: any}[]) {
201 baseCtors.forEach(baseCtor => {
202 Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
203 if (!derivedCtor.prototype[name])
204 derivedCtor.prototype[name] = baseCtor.prototype[name]
205 })
206 })
207}