Repository: numpy/numpy
File: numpy/_core/src/multiarray/unique.cpp (C++)

Code before:
            NPY_ANYORDER,
            NULL, // descr (use prototype's descr)
            1 // subok
        );
    }

    itemsize = PyArray_ITEMSIZE(self);
    PyArray_Descr *descr = PyArray_DESCR(self);
    char kind = descr->kind;
    // we only support booleans, integers, unsigned integers, and floats
    // we also only support data sizes present in our unique_funcs map
    if (
        (kind != 'b' && kind != 'i' && kind != 'u' && kind !='f')
        || (unique_funcs.find(itemsize) == unique_funcs.end())
    ){
        Py_XDECREF(self);
        return Py_None;
    }


    /* for the purpose of finding unique values on dtypes that we support, we
    don't really care what dtype it is, and we can look at the data as if they
    were all uint values */
    res = unique_funcs[itemsize](self);
    Py_XDECREF(self);
    return res;
}


// // The following is to expose the unique function to Python
// static PyMethodDef UniqueMethods[] = {
//     {"unique_hash",  PyArray_Unique, METH_VARARGS,
//      "Collect unique values via a hash map."},
//     {NULL, NULL, 0, NULL}
// };

// static struct PyModuleDef uniquemodule = {
//     PyModuleDef_HEAD_INIT,
//     "_multiarray_umath", /* name of module */
//     NULL, /* module docs */
//     -1,  /* size of per-interpreter state of the module,
//             or -1 if the module keeps state in global variables. */
//     UniqueMethods
// };

// PyMODINIT_FUNC
// PyInit_unique(void)
// {
//     PyArray_ImportNumPyAPI();
//     return PyModule_Create(&uniquemodule);
// }

Review comment (suggestion):
If you plug this into the main `unique` function, you should find some errors.  The point being: Maybe best not to try to be smart (bools are also a slight problem).

For floats the hash function has to account for `0` and `-0` being different (the NaN behavior may be trickier, since I am not sure C++ hashmap accounts for it, even if the float hashing does, does the comparison use something NaN aware?).

This all seems light-weight enough that I am happy to go there (not sure if unordered map is ideal, but probably good enough in practice; for one I am not sure if unordered map actually does hash randomization).

I am always a bit concerned that I would like to be able to provide a public API for others to implement this for their DTypes, and one that ideally doesn't use any NumPy array at all.  I think that would mean splitting this into 3-4 functions:
* One function which sets up the iterator and does the outer iteration (the `iternext`).  It then would call, unfortunately maybe 3 other functions (although that can all just be templated for now I think):
  * Create a new hash-map of the right type. 
  * Call one function which just deals with updating the hash-map for the 1-D slice (i.e. that `while (count--)` loop).
  * Split out the finalization of the hash-map into a NumPy array (or a single contiguous allocation).

If we had those, it should be straight forward to make them available to user DTypes.  Of course such a reorganization isn't terrible hard even if we don't do it now.

---

This code also needs handling with C++ exceptions (at the very least memory errors), so that is a bit of a problem, we may need to steal a generic translator (e.g. from `pybind11`), so that you can do `translate_errors_cpp_meth<PyArray_Unique>` when registering below.

Code after:
}


// this map contains the functions used for each item size.
typedef std::function<PyObject *(PyArrayObject *)> function_type;
std::unordered_map<int, function_type> unique_funcs = {
    {NPY_BYTE, unique<npy_byte>},
    {NPY_UBYTE, unique<npy_ubyte>},
    {NPY_SHORT, unique<npy_short>},
    {NPY_USHORT, unique<npy_ushort>},
    {NPY_INT, unique<npy_int>},
    {NPY_UINT, unique<npy_uint>},
    {NPY_LONG, unique<npy_long>},
    {NPY_ULONG, unique<npy_ulong>},
    {NPY_LONGLONG, unique<npy_longlong>},
    {NPY_ULONGLONG, unique<npy_ulonglong>},
    {NPY_INT8, unique<npy_int8>},
    {NPY_INT16, unique<npy_int16>},
    {NPY_INT32, unique<npy_int32>},
    {NPY_INT64, unique<npy_int64>},
    {NPY_UINT8, unique<npy_uint8>},
    {NPY_UINT16, unique<npy_uint16>},
    {NPY_UINT32, unique<npy_uint32>},
    {NPY_UINT64, unique<npy_uint64>},
    {NPY_DATETIME, unique<npy_uint64>},
};


/**
 * Python exposed implementation of `_unique_hash`.
 *
 * This is a C only function wrapping code that may cause C++ exceptions into
 * try/catch.
 *
 * @param arr NumPy array to find the unique values of.
 * @return Base-class NumPy array with unique values, `NotImplemented` if the
 * type is unsupported or `NULL` with an error set.
 */
extern "C" NPY_NO_EXPORT PyObject *
array__unique_hash(PyObject *NPY_UNUSED(module), PyObject *arr_obj)
{
    if (!PyArray_Check(arr_obj)) {
        PyErr_SetString(PyExc_TypeError,
                "_unique_hash() requires a NumPy array input.");
        return NULL;
    }
    PyArrayObject *arr = (PyArrayObject *)arr_obj;

    try {
        auto type = PyArray_TYPE(arr);
        // we only support data types present in our unique_funcs map